package browser

import (
	"strings"
	"testing"
)

// --- renderAXOutline: casos clave portados de render_ax_outline.py ---

func TestRenderAXOutline_ActionableRoleCarriesRef(t *testing.T) {
	nodes := []axNode{
		{nodeID: "1", role: "WebArea", name: "Page", childIDs: []string{"2"}},
		{nodeID: "2", backendDOMNodeID: "555", role: "button", name: "Submit"},
	}
	got := renderAXOutline(nodes, 0)
	want := "WebArea \"Page\"\n  button \"Submit\"  #ref=555"
	if got != want {
		t.Errorf("got:\n%q\nwant:\n%q", got, want)
	}
}

func TestRenderAXOutline_NonActionableHasNoRef(t *testing.T) {
	nodes := []axNode{
		{nodeID: "1", backendDOMNodeID: "9", role: "heading", name: "Title"},
	}
	got := renderAXOutline(nodes, 0)
	if strings.Contains(got, "#ref") {
		t.Errorf("rol no accionable no debe llevar #ref: %q", got)
	}
	if got != "heading \"Title\"" {
		t.Errorf("got %q", got)
	}
}

func TestRenderAXOutline_InputShowsValue(t *testing.T) {
	nodes := []axNode{
		{nodeID: "1", role: "form", childIDs: []string{"2"}},
		{nodeID: "2", backendDOMNodeID: "42", role: "textbox", name: "Email", value: "a@b.com"},
	}
	got := renderAXOutline(nodes, 0)
	want := "form\n  textbox \"Email\" = 'a@b.com'  #ref=42"
	if got != want {
		t.Errorf("got:\n%q\nwant:\n%q", got, want)
	}
}

func TestRenderAXOutline_ValueWithSingleQuoteUsesDoubleQuote(t *testing.T) {
	// Python repr: "it's" -> "it's" (comilla doble como delimitador).
	nodes := []axNode{
		{nodeID: "1", backendDOMNodeID: "7", role: "textbox", value: "it's"},
	}
	got := renderAXOutline(nodes, 0)
	want := "textbox = \"it's\"  #ref=7"
	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

func TestRenderAXOutline_SkipRoleElevatesChildren(t *testing.T) {
	// El nodo 'none' se omite; su hijo button sube al nivel del padre (depth 1,
	// no depth 2), porque el render del skip-node reusa el mismo depth.
	nodes := []axNode{
		{nodeID: "1", role: "WebArea", name: "Root", childIDs: []string{"2"}},
		{nodeID: "2", role: "none", childIDs: []string{"3"}},
		{nodeID: "3", backendDOMNodeID: "30", role: "button", name: "Go"},
	}
	got := renderAXOutline(nodes, 0)
	want := "WebArea \"Root\"\n  button \"Go\"  #ref=30"
	if got != want {
		t.Errorf("got:\n%q\nwant:\n%q", got, want)
	}
}

func TestRenderAXOutline_EmptyRoleElevatesChildren(t *testing.T) {
	nodes := []axNode{
		{nodeID: "1", role: "", childIDs: []string{"2"}}, // sin role: se omite
		{nodeID: "2", backendDOMNodeID: "20", role: "link", name: "Home"},
	}
	got := renderAXOutline(nodes, 0)
	// El nodo raiz sin role eleva su hijo a depth 0.
	want := "link \"Home\"  #ref=20"
	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

func TestRenderAXOutline_IndentationPerLevel(t *testing.T) {
	nodes := []axNode{
		{nodeID: "1", role: "WebArea", name: "A", childIDs: []string{"2"}},
		{nodeID: "2", role: "group", name: "B", childIDs: []string{"3"}},
		{nodeID: "3", role: "group", name: "C"},
	}
	got := renderAXOutline(nodes, 0)
	want := "WebArea \"A\"\n  group \"B\"\n    group \"C\""
	if got != want {
		t.Errorf("got:\n%q\nwant:\n%q", got, want)
	}
}

func TestRenderAXOutline_TruncationAddsSuffix(t *testing.T) {
	nodes := []axNode{
		{nodeID: "1", role: "WebArea", name: "AAAAAAAAAAAAAAAAAAAA"},
	}
	got := renderAXOutline(nodes, 10)
	if !strings.HasSuffix(got, "\n…[outline truncado]") {
		t.Errorf("falta sufijo de truncado: %q", got)
	}
	// El cuerpo truncado (sin sufijo) no debe exceder los 10 chars.
	body := strings.TrimSuffix(got, "\n…[outline truncado]")
	if len([]byte(body)) > 10 {
		t.Errorf("cuerpo truncado mas largo que maxChars: %q (%d bytes)", body, len(body))
	}
}

func TestRenderAXOutline_NoTruncationWhenUnderLimit(t *testing.T) {
	nodes := []axNode{{nodeID: "1", role: "button", name: "X", backendDOMNodeID: "1"}}
	got := renderAXOutline(nodes, 1000)
	if strings.Contains(got, "truncado") {
		t.Errorf("no debe truncar bajo el limite: %q", got)
	}
}

func TestRenderAXOutline_Empty(t *testing.T) {
	if got := renderAXOutline(nil, 0); got != "" {
		t.Errorf("nil -> %q, want vacio", got)
	}
}

func TestRenderAXOutline_RefFallsBackToNodeID(t *testing.T) {
	// Sin backendDOMNodeId, el #ref usa el nodeId.
	nodes := []axNode{
		{nodeID: "77", role: "button", name: "Fallback"},
	}
	got := renderAXOutline(nodes, 0)
	want := "button \"Fallback\"  #ref=77"
	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

func TestRenderAXOutline_CycleGuard(t *testing.T) {
	// Ciclo 1 -> 2 -> 1: no debe colgar ni duplicar nodos.
	nodes := []axNode{
		{nodeID: "1", role: "group", name: "A", childIDs: []string{"2"}},
		{nodeID: "2", role: "group", name: "B", childIDs: []string{"1"}},
	}
	got := renderAXOutline(nodes, 0)
	if strings.Count(got, "group \"A\"") != 1 {
		t.Errorf("nodo A renderizado mas de una vez: %q", got)
	}
}

// --- trimAXTree: casos clave portados de trim_ax_tree.py ---

func TestTrimAXTree_DiscardsIgnored(t *testing.T) {
	nodes := []axNode{
		{nodeID: "1", role: "button", name: "Keep"},
		{nodeID: "2", role: "button", name: "Drop", ignored: true},
	}
	got := trimAXTree(nodes)
	if len(got) != 1 || got[0].nodeID != "1" {
		t.Errorf("trim debe descartar ignored: %+v", got)
	}
}

func TestTrimAXTree_DiscardsEmptyGeneric(t *testing.T) {
	nodes := []axNode{
		{nodeID: "1", role: "generic"},              // sin name ni childIds -> descartado
		{nodeID: "2", role: "none"},                 // idem
		{nodeID: "3", role: "StaticText", name: ""}, // staticText vacio -> descartado
		{nodeID: "4", role: "StaticText", name: "Hola"},
	}
	got := trimAXTree(nodes)
	if len(got) != 1 || got[0].nodeID != "4" {
		t.Errorf("trim debe descartar generic/none/staticText vacios: %+v", got)
	}
}

func TestTrimAXTree_KeepsGenericWithChildren(t *testing.T) {
	nodes := []axNode{
		{nodeID: "1", role: "generic", childIDs: []string{"2"}}, // tiene hijos -> se queda
		{nodeID: "2", role: "button", name: "X"},
	}
	got := trimAXTree(nodes)
	if len(got) != 2 {
		t.Errorf("generic con hijos debe conservarse: %+v", got)
	}
}

func TestTrimAXTree_CollapsesSameRoleSingleChild(t *testing.T) {
	// list -> list (1 hijo, mismo role): se fusiona, el padre hereda los childIds.
	nodes := []axNode{
		{nodeID: "1", role: "list", childIDs: []string{"2"}},
		{nodeID: "2", role: "list", childIDs: []string{"3"}},
		{nodeID: "3", role: "listitem", name: "item"},
	}
	got := trimAXTree(nodes)
	// Nodo 2 desaparece; nodo 1 debe apuntar ahora a 3.
	var saw1, saw2 bool
	var node1 axNode
	for _, n := range got {
		if n.nodeID == "1" {
			saw1 = true
			node1 = n
		}
		if n.nodeID == "2" {
			saw2 = true
		}
	}
	if !saw1 || saw2 {
		t.Fatalf("colapso fallido: saw1=%v saw2=%v got=%+v", saw1, saw2, got)
	}
	if len(node1.childIDs) != 1 || node1.childIDs[0] != "3" {
		t.Errorf("padre fusionado debe heredar childIds del hijo: %+v", node1.childIDs)
	}
}

func TestTrimAXTree_PreservesOrder(t *testing.T) {
	nodes := []axNode{
		{nodeID: "3", role: "button", name: "C"},
		{nodeID: "1", role: "button", name: "A"},
		{nodeID: "2", role: "button", name: "B"},
	}
	got := trimAXTree(nodes)
	if len(got) != 3 || got[0].nodeID != "3" || got[1].nodeID != "1" || got[2].nodeID != "2" {
		t.Errorf("orden original no preservado: %+v", got)
	}
}

func TestTrimAXTree_Empty(t *testing.T) {
	if got := trimAXTree(nil); got != nil {
		t.Errorf("nil -> %+v, want nil", got)
	}
}

// --- axoPyRepr: paridad con Python repr() ---

func TestAxoPyRepr(t *testing.T) {
	cases := []struct{ in, want string }{
		{"hola", "'hola'"},
		{"it's", "\"it's\""},                   // tiene ', no " -> delimitador "
		{"say \"hi\"", "'say \"hi\"'"},         // tiene " -> delimitador '
		{"both ' and \"", "'both \\' and \"'"}, // ambos -> ' con escape del '
		{"a\nb", "'a\\nb'"},
		{"back\\slash", "'back\\\\slash'"},
	}
	for _, c := range cases {
		if got := axoPyRepr(c.in); got != c.want {
			t.Errorf("axoPyRepr(%q) = %q, want %q", c.in, got, c.want)
		}
	}
}

// --- axoParseNodes: extraccion del map CDP (numeros como float64) ---

func TestAxoParseNodes(t *testing.T) {
	result := map[string]any{
		"nodes": []any{
			map[string]any{
				"nodeId":           "1",
				"backendDOMNodeId": float64(555), // CDP int llega como float64
				"ignored":          false,
				"role":             map[string]any{"value": "button"},
				"name":             map[string]any{"value": "Go"},
				"value":            map[string]any{"value": "x"},
				"childIds":         []any{"2", "3"},
			},
		},
	}
	got := axoParseNodes(result)
	if len(got) != 1 {
		t.Fatalf("got %d nodos, want 1", len(got))
	}
	n := got[0]
	if n.nodeID != "1" || n.backendDOMNodeID != "555" || n.role != "button" ||
		n.name != "Go" || n.value != "x" || len(n.childIDs) != 2 {
		t.Errorf("parse incorrecto: %+v", n)
	}
}
