// Tests del color picker (Modal personalizado dentro de Menu/Popover de Mantine).
// Reproduce el bug: click en el circulo "Color personalizado" abre Modal pero
// se cierra inmediatamente. Comprueba que el Modal permanezca visible >300ms.
package e2e

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"testing"
	"time"

	"fn-registry/functions/browser"
)

// loginAndGetCookie registra (idempotente) y hace login. Retorna valor de la cookie kanban_session.
func loginAndGetCookie(t *testing.T, baseURL, user, pass string) string {
	t.Helper()
	body := fmt.Sprintf(`{"username":%q,"password":%q,"display_name":%q}`, user, pass, user)
	// Registro: 200 OK la primera vez, error si ya existe (ignorable).
	_, _ = http.Post(baseURL+"/api/auth/register", "application/json", strings.NewReader(body))

	loginBody := fmt.Sprintf(`{"username":%q,"password":%q}`, user, pass)
	resp, err := http.Post(baseURL+"/api/auth/login", "application/json", strings.NewReader(loginBody))
	if err != nil {
		t.Fatalf("login http: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		buf := new(bytes.Buffer)
		buf.ReadFrom(resp.Body)
		t.Fatalf("login status %d: %s", resp.StatusCode, buf.String())
	}
	for _, ck := range resp.Cookies() {
		if ck.Name == "kanban_session" {
			return ck.Value
		}
	}
	t.Fatalf("login no devolvio cookie kanban_session")
	return ""
}

// ensureBoardSeed crea una columna y card si la BD esta vacia. Usa la cookie autenticada.
func ensureBoardSeed(t *testing.T, baseURL, cookie string) {
	t.Helper()
	client := &http.Client{}
	mk := func(method, url string, body string) *http.Response {
		req, _ := http.NewRequest(method, baseURL+url, strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.AddCookie(&http.Cookie{Name: "kanban_session", Value: cookie})
		resp, err := client.Do(req)
		if err != nil {
			t.Fatalf("%s %s: %v", method, url, err)
		}
		return resp
	}
	// Lee board.
	resp := mk("GET", "/api/board", "")
	defer resp.Body.Close()
	var board struct {
		Columns []map[string]any `json:"columns"`
		Cards   []map[string]any `json:"cards"`
	}
	json.NewDecoder(resp.Body).Decode(&board)

	var colID string
	if len(board.Columns) == 0 {
		r := mk("POST", "/api/columns", `{"name":"e2e"}`)
		var c map[string]any
		json.NewDecoder(r.Body).Decode(&c)
		r.Body.Close()
		colID = c["id"].(string)
	} else {
		colID = board.Columns[0]["id"].(string)
	}
	if len(board.Cards) == 0 {
		r := mk("POST", "/api/cards", fmt.Sprintf(`{"column_id":%q,"title":"e2e card"}`, colID))
		r.Body.Close()
	}
}

// authedSetup hace login + inyecta cookie en el browser CDP.
func authedSetup(t *testing.T) (*ctx, string) {
	t.Helper()
	c := setup(t)
	user := envOr("KANBAN_USER", "e2etest")
	pass := envOr("KANBAN_PASS", "e2etest")
	cookie := loginAndGetCookie(t, c.baseURL, user, pass)
	ensureBoardSeed(t, c.baseURL, cookie)

	// Navegar a la home primero para que el browser tenga el dominio en su jar.
	c.navigate("/")
	host := strings.TrimPrefix(strings.TrimPrefix(c.baseURL, "http://"), "https://")
	host = strings.SplitN(host, ":", 2)[0]
	if err := browser.CdpSetCookie(c.conn, "kanban_session", cookie, host, "/", true); err != nil {
		t.Fatalf("set_cookie: %v", err)
	}
	c.navigate("/")
	return c, cookie
}

func TestColorPicker_AvatarMenu_ModalStaysOpen(t *testing.T) {
	c, _ := authedSetup(t)

	// Esperar avatar (header).
	if err := browser.CdpWaitElement(c.conn, "[aria-label='Usuario']", 8*time.Second); err != nil {
		c.screenshot("debug_no_avatar")
		t.Fatalf("avatar no aparecio (login fallo?): %v", err)
	}
	if err := browser.CdpClick(c.conn, "[aria-label='Usuario']"); err != nil {
		t.Fatalf("click avatar: %v", err)
	}
	// Esperar el grid de colores.
	if err := browser.CdpWaitElement(c.conn, "[aria-label='Color personalizado']", 5*time.Second); err != nil {
		c.screenshot("debug_no_picker_grid")
		t.Fatalf("picker grid no visible: %v", err)
	}
	c.screenshot("avatar_menu_open")

	// Click "+".
	if err := browser.CdpClick(c.conn, "[aria-label='Color personalizado']"); err != nil {
		t.Fatalf("click +: %v", err)
	}
	// Esperar 350ms — si el bug persiste, el modal habra desaparecido.
	time.Sleep(350 * time.Millisecond)
	c.screenshot("avatar_after_plus_click")

	// Mantine Modal renderiza con role="dialog". Comprobar visible.
	val := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	t.Logf("dialogs en DOM tras 350ms: %s", val)
	if strings.Contains(val, "0") {
		// Bug confirmado: modal cerro.
		t.Errorf("BUG: Modal del color picker se cerro inmediatamente (avatar menu)")
	}

	// Comprobar header del modal.
	val = c.eval(`(() => { const m = document.querySelector('[role="dialog"] .mantine-Modal-title'); return m ? m.textContent : 'NULL'; })()`)
	t.Logf("modal title: %s", val)
}

func TestColorPicker_AvatarModal_ClicksInsideKeepOpen(t *testing.T) {
	c, _ := authedSetup(t)

	if err := browser.CdpWaitElement(c.conn, "[aria-label='Usuario']", 8*time.Second); err != nil {
		t.Fatalf("avatar: %v", err)
	}
	if err := browser.CdpClick(c.conn, "[aria-label='Usuario']"); err != nil {
		t.Fatalf("click avatar: %v", err)
	}
	if err := browser.CdpWaitElement(c.conn, "[aria-label='Color personalizado']", 5*time.Second); err != nil {
		t.Fatalf("picker grid: %v", err)
	}
	if err := browser.CdpClick(c.conn, "[aria-label='Color personalizado']"); err != nil {
		t.Fatalf("click +: %v", err)
	}
	if err := browser.CdpWaitElement(c.conn, "[role='dialog']", 2*time.Second); err != nil {
		t.Fatalf("modal: %v", err)
	}

	// Click 1: input hex
	if err := browser.CdpClick(c.conn, "[role='dialog'] input"); err != nil {
		t.Fatalf("click input: %v", err)
	}
	time.Sleep(150 * time.Millisecond)
	val := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	if strings.Contains(val, "0") {
		c.screenshot("modal_closed_after_input_click")
		t.Errorf("BUG: modal cerro tras click en input hex (dialogs=%s)", val)
	}

	// Click 2: zona saturation del ColorPicker
	if err := browser.CdpClick(c.conn, "[role='dialog'] .mantine-ColorPicker-saturation"); err != nil {
		t.Logf("click saturation no fue posible: %v", err)
	} else {
		time.Sleep(150 * time.Millisecond)
		val = c.eval(`document.querySelectorAll('[role="dialog"]').length`)
		if strings.Contains(val, "0") {
			c.screenshot("modal_closed_after_saturation")
			t.Errorf("BUG: modal cerro tras click en saturation (dialogs=%s)", val)
		}
	}

	// Click 3: swatch
	val = c.eval(`(() => { const s = document.querySelector('[role="dialog"] .mantine-ColorPicker-swatch'); if (!s) return 'NO_SWATCH'; s.click(); return 'OK'; })()`)
	t.Logf("swatch click: %s", val)
	time.Sleep(150 * time.Millisecond)
	val = c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	if strings.Contains(val, "0") {
		c.screenshot("modal_closed_after_swatch")
		t.Errorf("BUG: modal cerro tras click en swatch (dialogs=%s)", val)
	}

	// Click 4: titulo del modal (zona muerta)
	val = c.eval(`(() => { const t = document.querySelector('.mantine-Modal-title'); if (!t) return 'NO_TITLE'; t.click(); return 'OK'; })()`)
	t.Logf("title click: %s", val)
	time.Sleep(150 * time.Millisecond)
	val = c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	if strings.Contains(val, "0") {
		c.screenshot("modal_closed_after_title")
		t.Errorf("BUG: modal cerro tras click en title (dialogs=%s)", val)
	}

	c.screenshot("modal_after_all_clicks")
}

// Simula drag desde dentro del ColorPicker hasta fuera del modal,
// que es el patron de uso humano cuando arrastra el saturation.
func TestColorPicker_DragInsideThenOutside_StaysOpen(t *testing.T) {
	c, _ := authedSetup(t)

	if err := browser.CdpWaitElement(c.conn, "[aria-label='Usuario']", 8*time.Second); err != nil {
		t.Fatalf("avatar: %v", err)
	}
	if err := browser.CdpClick(c.conn, "[aria-label='Usuario']"); err != nil {
		t.Fatalf("click avatar: %v", err)
	}
	if err := browser.CdpWaitElement(c.conn, "[aria-label='Color personalizado']", 5*time.Second); err != nil {
		t.Fatalf("picker grid: %v", err)
	}
	if err := browser.CdpClick(c.conn, "[aria-label='Color personalizado']"); err != nil {
		t.Fatalf("click +: %v", err)
	}
	if err := browser.CdpWaitElement(c.conn, "[role='dialog'] .mantine-ColorPicker-saturation", 3*time.Second); err != nil {
		t.Fatalf("saturation no aparecio: %v", err)
	}

	// Despachar drag manual via JS: pointerdown sat, pointermove out, pointerup out.
	out := c.eval(`(() => {
		const sat = document.querySelector('[role="dialog"] .mantine-ColorPicker-saturation');
		if (!sat) return 'NO_SAT';
		const r = sat.getBoundingClientRect();
		const startX = r.left + r.width / 2;
		const startY = r.top + r.height / 2;
		const endX = r.left - 200; // fuera del modal por la izquierda
		const endY = r.top - 200;  // fuera por arriba
		const fire = (target, type, x, y) => {
			const ev = new PointerEvent(type, {
				bubbles: true, cancelable: true, composed: true,
				clientX: x, clientY: y, pointerId: 1, pointerType: 'mouse', isPrimary: true, button: 0, buttons: 1,
			});
			target.dispatchEvent(ev);
			const m = new MouseEvent(type === 'pointerdown' ? 'mousedown' : type === 'pointerup' ? 'mouseup' : 'mousemove', {
				bubbles: true, cancelable: true, view: window,
				clientX: x, clientY: y, button: 0, buttons: type === 'pointerup' ? 0 : 1,
			});
			target.dispatchEvent(m);
		};
		fire(sat, 'pointerdown', startX, startY);
		// Mover en pasos
		for (let i = 1; i <= 10; i++) {
			const x = startX + (endX - startX) * i / 10;
			const y = startY + (endY - startY) * i / 10;
			const elAt = document.elementFromPoint(x, y) || document;
			fire(elAt, 'pointermove', x, y);
		}
		const finalEl = document.elementFromPoint(endX, endY) || document;
		fire(finalEl, 'pointerup', endX, endY);
		return 'OK';
	})()`)
	t.Logf("drag result: %s", out)

	time.Sleep(200 * time.Millisecond)
	val := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	t.Logf("dialogs tras drag fuera: %s", val)
	c.screenshot("modal_after_drag_outside")
	if strings.Contains(val, "0") {
		t.Errorf("BUG: modal cerro tras drag desde saturation hasta fuera del modal")
	}

	// Click en una zona vacia del modal (no input, no buttons).
	val = c.eval(`(() => {
		const dlg = document.querySelector('[role="dialog"]');
		if (!dlg) return 'NO_DLG';
		const r = dlg.getBoundingClientRect();
		// Click en el header del modal (margen superior).
		const x = r.left + 10;
		const y = r.top + 10;
		const target = document.elementFromPoint(x, y);
		if (!target) return 'NO_TARGET';
		const ev = new MouseEvent('click', { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y });
		target.dispatchEvent(ev);
		return 'OK target=' + target.tagName + '.' + target.className;
	})()`)
	t.Logf("click header zone: %s", val)
	time.Sleep(150 * time.Millisecond)
	val = c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	t.Logf("dialogs tras click header: %s", val)
	if strings.Contains(val, "0") {
		c.screenshot("modal_closed_after_header_click")
		t.Errorf("BUG: modal cerro tras click en header del modal")
	}
}

func jsonQuote(s string) string {
	b, _ := json.Marshal(s)
	return string(b)
}

// Click en cada region clickeable del modal — verifica que ninguna cierre.
func TestColorPicker_AllRegionsKeepModalOpen(t *testing.T) {
	c, _ := authedSetup(t)
	if err := browser.CdpWaitElement(c.conn, "[aria-label='Usuario']", 8*time.Second); err != nil {
		t.Fatalf("avatar: %v", err)
	}
	browser.CdpClick(c.conn, "[aria-label='Usuario']")
	browser.CdpWaitElement(c.conn, "[aria-label='Color personalizado']", 5*time.Second)
	browser.CdpClick(c.conn, "[aria-label='Color personalizado']")
	browser.CdpWaitElement(c.conn, "[role='dialog']", 3*time.Second)

	regions := []struct {
		name     string
		selector string
	}{
		{"body padding", "[role='dialog'] .mantine-Modal-body"},
		{"saturation", "[role='dialog'] .mantine-ColorPicker-saturation"},
		{"hue slider", "[role='dialog'] .mantine-ColorPicker-slider"},
		{"swatch 0", "[role='dialog'] .mantine-ColorPicker-swatch"},
		{"hex input", "[role='dialog'] input"},
		{"hex label", "[role='dialog'] .mantine-TextInput-label"},
		{"stack gap", "[role='dialog'] .mantine-Stack-root"},
	}
	for _, r := range regions {
		v := c.eval(`(() => {
			const el = document.querySelector(` + jsonQuote(r.selector) + `);
			if (!el) return 'NO_EL';
			const rect = el.getBoundingClientRect();
			const x = rect.left + rect.width / 2;
			const y = rect.top + rect.height / 2;
			const target = document.elementFromPoint(x, y);
			if (!target) return 'NO_TARGET';
			['pointerdown','mousedown','pointerup','mouseup','click'].forEach(type => {
				const Ctor = type.startsWith('pointer') ? PointerEvent : MouseEvent;
				const ev = new Ctor(type, { bubbles: true, cancelable: true, composed: true, clientX: x, clientY: y, button: 0, view: window });
				target.dispatchEvent(ev);
			});
			return 'OK ' + target.tagName + '.' + (target.className || '').slice(0, 40);
		})()`)
		t.Logf("region %s: %s", r.name, v)
		time.Sleep(80 * time.Millisecond)
		count := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
		if strings.Contains(count, "0") {
			c.screenshot("modal_closed_at_" + strings.ReplaceAll(r.name, " ", "_"))
			t.Errorf("BUG: modal cerro tras click en region %q", r.name)
			break
		}
	}
}

// Verifica que clicar el ColorPicker en zonas de uso real (dragging del saturation,
// click en el slider de hue, click en swatches) NO cierre el modal.
// Sleep extra de 600ms tras cada accion para esperar transiciones Mantine.
func TestColorPicker_RealisticInteractions(t *testing.T) {
	c, _ := authedSetup(t)
	browser.CdpWaitElement(c.conn, "[aria-label='Usuario']", 8*time.Second)
	browser.CdpClick(c.conn, "[aria-label='Usuario']")
	time.Sleep(300 * time.Millisecond)
	browser.CdpWaitElement(c.conn, "[aria-label='Color personalizado']", 5*time.Second)
	browser.CdpClick(c.conn, "[aria-label='Color personalizado']")
	browser.CdpWaitElement(c.conn, "[role='dialog'] .mantine-ColorPicker-saturation", 3*time.Second)
	time.Sleep(500 * time.Millisecond) // animacion modal entrada

	// 1. Drag DENTRO del saturation (movimientos cortos, sin salir)
	out := c.eval(`(() => {
		const sat = document.querySelector('[role="dialog"] .mantine-ColorPicker-saturation');
		if (!sat) return 'NO_SAT';
		const r = sat.getBoundingClientRect();
		const mid = (axis) => axis === 'x' ? r.left + r.width / 2 : r.top + r.height / 2;
		const fire = (target, type, x, y) => {
			const Ctor = type.startsWith('pointer') ? PointerEvent : MouseEvent;
			target.dispatchEvent(new Ctor(type, { bubbles: true, cancelable: true, composed: true, clientX: x, clientY: y, button: 0, buttons: type === 'pointerup' || type === 'mouseup' ? 0 : 1, view: window, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
		};
		fire(sat, 'pointerdown', mid('x'), mid('y'));
		fire(sat, 'mousedown', mid('x'), mid('y'));
		for (let i = 0; i < 5; i++) {
			const x = r.left + (r.width * (0.3 + i * 0.1));
			const y = r.top + (r.height * (0.3 + i * 0.1));
			fire(sat, 'pointermove', x, y);
			fire(sat, 'mousemove', x, y);
		}
		fire(sat, 'pointerup', mid('x'), mid('y'));
		fire(sat, 'mouseup', mid('x'), mid('y'));
		return 'OK';
	})()`)
	t.Logf("drag interno saturation: %s", out)
	time.Sleep(600 * time.Millisecond)
	c.screenshot("after_drag_internal")
	val := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	t.Logf("dialogs tras drag interno (600ms): %s", val)
	if strings.Contains(val, "0") {
		t.Errorf("BUG: modal cerro tras drag interno de saturation")
	}

	// 2. Verificar que Mantine NO añade close button (X) — debe estar deshabilitado.
	closeBtn := c.eval(`document.querySelectorAll('[role="dialog"] .mantine-Modal-close').length`)
	t.Logf("close buttons en modal: %s", closeBtn)
	if !strings.Contains(closeBtn, "0") {
		t.Errorf("BUG: modal tiene close button (X). Click accidental cierra. Usar withCloseButton={false}")
	}
}

// Helper: abre modal avatar y devuelve ctx con modal listo.
func openAvatarColorModal(t *testing.T) *ctx {
	t.Helper()
	c, _ := authedSetup(t)
	browser.CdpWaitElement(c.conn, "[aria-label='Usuario']", 8*time.Second)
	browser.CdpClick(c.conn, "[aria-label='Usuario']")
	time.Sleep(300 * time.Millisecond)
	browser.CdpWaitElement(c.conn, "[aria-label='Color personalizado']", 5*time.Second)
	browser.CdpClick(c.conn, "[aria-label='Color personalizado']")
	browser.CdpWaitElement(c.conn, "[role='dialog']", 3*time.Second)
	time.Sleep(500 * time.Millisecond)
	return c
}

// Comportamiento deseado: clicks DENTRO del modal NO cierran. Tests granulares
// con sleep generoso para esperar animaciones de Mantine.
func TestColorPicker_InsideClicks_DoNotClose(t *testing.T) {
	c := openAvatarColorModal(t)

	regions := []struct {
		name     string
		selector string
	}{
		{"hex_input", "[role='dialog'] input"},
		{"hex_label", "[role='dialog'] .mantine-TextInput-label"},
		{"saturation_center", "[role='dialog'] .mantine-ColorPicker-saturation"},
		{"hue_slider", "[role='dialog'] .mantine-ColorPicker-slider"},
		{"swatch_first", "[role='dialog'] .mantine-ColorPicker-swatch"},
		{"body", "[role='dialog'] .mantine-Modal-body"},
		{"stack", "[role='dialog'] .mantine-Stack-root"},
		{"title", "[role='dialog'] .mantine-Modal-title"},
		{"header", "[role='dialog'] .mantine-Modal-header"},
		{"content", "[role='dialog']"},
	}
	for _, r := range regions {
		t.Run(r.name, func(t *testing.T) {
			res := c.eval(`(() => {
				const el = document.querySelector(` + jsonQuote(r.selector) + `);
				if (!el) return 'NO_EL';
				const rc = el.getBoundingClientRect();
				const x = rc.left + Math.min(rc.width / 2, 30);
				const y = rc.top + Math.min(rc.height / 2, 12);
				const target = document.elementFromPoint(x, y);
				if (!target) return 'NO_TARGET';
				['pointerdown','mousedown','pointerup','mouseup','click'].forEach(type => {
					const Ctor = type.startsWith('pointer') ? PointerEvent : MouseEvent;
					target.dispatchEvent(new Ctor(type, { bubbles: true, cancelable: true, composed: true, clientX: x, clientY: y, button: 0, buttons: type.includes('up') ? 0 : 1, view: window, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
				});
				return 'OK ' + target.tagName;
			})()`)
			t.Logf("region %s: %s", r.name, res)
			time.Sleep(500 * time.Millisecond) // esperar animaciones
			count := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
			if strings.Contains(count, "0") {
				c.screenshot("inside_closed_" + r.name)
				t.Errorf("BUG: modal cerro tras click en %q", r.name)
			}
		})
	}
}

// Click en overlay (zona oscura fuera del panel del modal) DEBE cerrar.
func TestColorPicker_OverlayClick_Closes(t *testing.T) {
	c := openAvatarColorModal(t)
	res := c.eval(`(() => {
		const overlay = document.querySelector('.mantine-Overlay-root, .mantine-Modal-overlay');
		if (!overlay) return 'NO_OVERLAY';
		const rc = overlay.getBoundingClientRect();
		// click en esquina superior izquierda del overlay (lejos del modal centrado)
		const x = rc.left + 10;
		const y = rc.top + 10;
		const target = document.elementFromPoint(x, y);
		if (!target) return 'NO_TARGET';
		['pointerdown','mousedown','pointerup','mouseup','click'].forEach(type => {
			const Ctor = type.startsWith('pointer') ? PointerEvent : MouseEvent;
			target.dispatchEvent(new Ctor(type, { bubbles: true, cancelable: true, composed: true, clientX: x, clientY: y, button: 0, view: window, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
		});
		return 'OK ' + target.tagName + '.' + (target.className || '').slice(0,30);
	})()`)
	t.Logf("overlay click: %s", res)
	time.Sleep(500 * time.Millisecond)
	count := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	if !strings.Contains(count, "0") {
		c.screenshot("overlay_did_not_close")
		t.Errorf("BUG: modal NO cerro tras click en overlay (esperado: cierra)")
	}
}

// Boton Cancelar DEBE cerrar.
func TestColorPicker_CancelButton_Closes(t *testing.T) {
	c := openAvatarColorModal(t)
	res := c.eval(`(() => { const b = [...document.querySelectorAll('[role="dialog"] button')].find(x => x.textContent.trim() === 'Cancelar'); if (!b) return 'NO_BTN'; b.click(); return 'OK'; })()`)
	t.Logf("cancelar: %s", res)
	time.Sleep(500 * time.Millisecond)
	count := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	if !strings.Contains(count, "0") {
		t.Errorf("BUG: modal NO cerro tras Cancelar")
	}
}

// Boton Aceptar DEBE cerrar.
func TestColorPicker_AcceptButton_Closes(t *testing.T) {
	c := openAvatarColorModal(t)
	res := c.eval(`(() => { const b = [...document.querySelectorAll('[role="dialog"] button')].find(x => x.textContent.trim() === 'Aceptar'); if (!b) return 'NO_BTN'; b.click(); return 'OK'; })()`)
	t.Logf("aceptar: %s", res)
	time.Sleep(500 * time.Millisecond)
	count := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	if !strings.Contains(count, "0") {
		t.Errorf("BUG: modal NO cerro tras Aceptar")
	}
}

// Modal NO debe tener close button (X) — clicks accidentales cierran.
func TestColorPicker_NoXCloseButton(t *testing.T) {
	c := openAvatarColorModal(t)
	count := c.eval(`document.querySelectorAll('[role="dialog"] .mantine-Modal-close').length`)
	t.Logf("X buttons: %s", count)
	if !strings.Contains(count, "0") {
		t.Errorf("BUG: modal tiene close button (X). Quitar withCloseButton={false}")
	}
}

func TestColorPicker_CardMenu_ModalStaysOpen(t *testing.T) {
	c, _ := authedSetup(t)

	// Esperar al menos una card.
	if err := browser.CdpWaitElement(c.conn, "[aria-label='Acciones']", 8*time.Second); err != nil {
		c.screenshot("debug_no_card")
		t.Fatalf("card menu trigger no visible: %v", err)
	}
	if err := browser.CdpClick(c.conn, "[aria-label='Acciones']"); err != nil {
		t.Fatalf("click card menu: %v", err)
	}
	time.Sleep(150 * time.Millisecond)

	// Click submenu Color.
	val := c.eval(`(() => { const items = [...document.querySelectorAll('.mantine-Menu-item')]; const t = items.find(i => i.textContent.trim() === 'Color'); if (!t) return 'NOT_FOUND'; t.click(); return 'OK'; })()`)
	if !strings.Contains(val, "OK") {
		c.screenshot("debug_no_color_item")
		t.Fatalf("item Color no encontrado en menu: %s", val)
	}
	time.Sleep(200 * time.Millisecond)
	c.screenshot("card_color_popover_open")

	// Inyectar capturador de logs ahora (despues de la nav, antes del click).
	c.eval(`(() => { window.__logs = []; const orig = console.log; console.log = function(...a) { window.__logs.push(a.map(x => typeof x === 'object' ? JSON.stringify(x) : String(x)).join(' ')); orig.apply(console, a); }; })()`)

	// Diagnostico DOM: cuantos "+" hay y donde estan?
	plus := c.eval(`document.querySelectorAll('[aria-label="Color personalizado"]').length`)
	t.Logf("'+' en DOM: %s", plus)
	plusVisible := c.eval(`(() => { const el = document.querySelector('[aria-label="Color personalizado"]'); if (!el) return 'NO_EL'; const r = el.getBoundingClientRect(); return JSON.stringify({x: r.x, y: r.y, w: r.width, h: r.height}); })()`)
	t.Logf("'+' rect: %s", plusVisible)

	// Click "+" custom.
	if err := browser.CdpClick(c.conn, "[aria-label='Color personalizado']"); err != nil {
		t.Fatalf("click + en card popover: %v", err)
	}
	// Sondear cada 50ms hasta 800ms para ver si el modal aparece y luego desaparece.
	for i := 0; i < 16; i++ {
		time.Sleep(50 * time.Millisecond)
		v := c.eval(`document.querySelectorAll('[role="dialog"]').length`)
		t.Logf("[%dms] dialogs=%s", (i+1)*50, v)
	}
	c.screenshot("card_after_plus_click")

	val = c.eval(`document.querySelectorAll('[role="dialog"]').length`)
	t.Logf("dialogs final (card): %s", val)
	logs := c.eval(`JSON.stringify(window.__logs || [])`)
	t.Logf("console logs: %s", logs)
	if strings.Contains(val, "0") {
		t.Errorf("BUG: Modal del color picker se cerro inmediatamente (card menu)")
	}
}
