package browser

import (
	"strings"
	"testing"
	"time"
)

// TestCdpWaitIdleDefaults verifica el comportamiento observable de CdpWaitIdle
// sin requerir una instancia Chrome real.
func TestCdpWaitIdleDefaults(t *testing.T) {
	t.Run("conexion nula retorna error inmediato", func(t *testing.T) {
		err := CdpWaitIdle(nil, CdpWaitIdleOpts{})
		if err == nil {
			t.Fatal("esperaba error para conexion nula, got nil")
		}
	})

	t.Run("opts con ceros aplica defaults antes de usar", func(t *testing.T) {
		// Zero-value de CdpWaitIdleOpts debe tener todos los campos en 0
		// para que la logica de defaults sea alcanzable.
		var opts CdpWaitIdleOpts
		if opts.QuietMs != 0 || opts.Timeout != 0 || opts.MaxInflight != 0 || opts.PollMs != 0 {
			t.Fatal("zero-value de CdpWaitIdleOpts debe tener todos los campos en 0")
		}
	})

	t.Run("mensaje de error nil-conn menciona cdp wait idle", func(t *testing.T) {
		err := CdpWaitIdle(nil, CdpWaitIdleOpts{
			QuietMs: 100,
			Timeout: 500 * time.Millisecond,
			PollMs:  50,
		})
		if err == nil {
			t.Fatal("esperaba error, got nil")
		}
		if !strings.Contains(err.Error(), "cdp wait idle") {
			t.Errorf("mensaje de error %q no contiene 'cdp wait idle'", err.Error())
		}
	})
}

// TestInflightTracker cubre el nucleo puro del contador de red. No requiere Chrome:
// alimenta secuencias de eventos {requestId, resourceType} y verifica el conteo.
func TestInflightTracker(t *testing.T) {
	t.Run("golden: carga normal llega a idle", func(t *testing.T) {
		tr := NewInflightTracker()
		tr.OnRequest("r1", "Document")
		tr.OnRequest("r2", "Script")
		tr.OnRequest("r3", "Image")
		if got := tr.Inflight(); got != 3 {
			t.Fatalf("inflight tras 3 requests = %d, esperaba 3", got)
		}
		tr.OnFinish("r1")
		tr.OnFinish("r2")
		tr.OnFail("r3") // un recurso que falla tambien deja de estar en vuelo
		if got := tr.Inflight(); got != 0 {
			t.Fatalf("inflight tras completar todo = %d, esperaba 0", got)
		}
		if !tr.IsIdle(0) {
			t.Error("esperaba IsIdle(0)=true con inflight=0")
		}
	})

	t.Run("edge: analytics residual idle ok con MaxInflight=2", func(t *testing.T) {
		tr := NewInflightTracker()
		// La pagina cargo, pero 2 beacons de analytics quedan sin finalizar.
		tr.OnRequest("doc", "Document")
		tr.OnFinish("doc")
		tr.OnRequest("beacon1", "Ping")
		tr.OnRequest("beacon2", "XHR")
		if got := tr.Inflight(); got != 2 {
			t.Fatalf("inflight = %d, esperaba 2 (beacons residuales)", got)
		}
		if tr.IsIdle(0) {
			t.Error("con MaxInflight=0 NO deberia ser idle (2 beacons en vuelo)")
		}
		if !tr.IsIdle(2) {
			t.Error("con MaxInflight=2 (default) SI deberia ser idle")
		}
	})

	t.Run("error/regresion: WebSocket abierto NO impide idle", func(t *testing.T) {
		tr := NewInflightTracker()
		tr.OnRequest("doc", "Document")
		tr.OnFinish("doc")
		// Un stream WebSocket se abre y nunca emite loadingFinished.
		tr.OnRequest("ws1", "WebSocket")
		// Un EventSource (SSE) tampoco termina.
		tr.OnRequest("sse1", "EventSource")
		if got := tr.Inflight(); got != 0 {
			t.Fatalf("inflight = %d, esperaba 0 (WS/SSE excluidos)", got)
		}
		if !tr.IsIdle(0) {
			t.Error("con WS+SSE abiertos pero excluidos, deberia ser idle absoluto")
		}
	})

	t.Run("edge: finish de request no trackeado es no-op (no va negativo)", func(t *testing.T) {
		tr := NewInflightTracker()
		// loadingFinished de un requestId que nunca contamos (p.ej. el handshake
		// de un WebSocket excluido) no debe romper el conteo.
		tr.OnFinish("desconocido")
		tr.OnFail("ws-handshake")
		if got := tr.Inflight(); got != 0 {
			t.Fatalf("inflight = %d, esperaba 0 (no negativo)", got)
		}
		tr.OnRequest("r1", "Fetch")
		if got := tr.Inflight(); got != 1 {
			t.Fatalf("inflight tras un request real = %d, esperaba 1", got)
		}
	})

	t.Run("edge: requestId duplicado no infla el conteo", func(t *testing.T) {
		tr := NewInflightTracker()
		tr.OnRequest("r1", "Fetch")
		tr.OnRequest("r1", "Fetch") // mismo id (redirect re-emite)
		if got := tr.Inflight(); got != 1 {
			t.Fatalf("inflight = %d, esperaba 1 (id deduplicado)", got)
		}
	})
}
