package correlation_test

import (
	"context"
	"testing"
	"time"

	"github.com/antitbone/ja4/ja4ebpf/internal/correlation"
)

func TestSessionStateAddRequest(t *testing.T) {
	s := &correlation.SessionState{
		Key:          correlation.SessionKey{SrcIP: [4]byte{1, 2, 3, 4}, SrcPort: 12345},
		FirstSeen:    time.Now(),
		LastActivity: time.Now(),
	}

	req := correlation.HTTPRequest{
		Method:    "GET",
		Path:      "/api/test",
		Timestamp: time.Now(),
	}

	s.AddRequest(req)

	if len(s.Requests) != 1 {
		t.Errorf("attendu 1 requête, obtenu %d", len(s.Requests))
	}
	if s.Requests[0].Method != "GET" {
		t.Errorf("méthode attendue GET, obtenue %s", s.Requests[0].Method)
	}
}

func TestSessionStateMultipleRequests(t *testing.T) {
	s := &correlation.SessionState{
		FirstSeen:    time.Now(),
		LastActivity: time.Now(),
	}

	for i := 0; i < 3; i++ {
		s.AddRequest(correlation.HTTPRequest{
			Method:    "GET",
			Path:      "/path",
			Timestamp: time.Now(),
		})
	}

	if len(s.Requests) != 3 {
		t.Errorf("attendu 3 requêtes, obtenu %d", len(s.Requests))
	}
}

func TestSessionStateIsExpired(t *testing.T) {
	past := time.Now().Add(-1 * time.Second)
	s := &correlation.SessionState{
		LastActivity: past,
	}

	if !s.IsExpired(500 * time.Millisecond) {
		t.Error("session inactive depuis 1s doit être expirée avec timeout 500ms")
	}
	if s.IsExpired(2 * time.Second) {
		t.Error("session inactive depuis 1s ne doit pas être expirée avec timeout 2s")
	}
}

func TestSessionStateIsSlowlorisOpenConnection(t *testing.T) {
	s := &correlation.SessionState{
		FirstSeen:    time.Now().Add(-15 * time.Second),
		LastActivity: time.Now(),
	}
	// Connexion ouverte sans requête depuis 15s → Slowloris
	if !s.IsSlowloris(10 * time.Second) {
		t.Error("connexion sans requête depuis 15s doit être détectée comme Slowloris")
	}
}

func TestSessionStateIsSlowlorisWithRequest(t *testing.T) {
	s := &correlation.SessionState{
		FirstSeen:    time.Now().Add(-30 * time.Second),
		LastActivity: time.Now(),
	}
	// Requête complète présente → pas Slowloris (len(Requests) > 0)
	s.AddRequest(correlation.HTTPRequest{
		Method:    "GET",
		Path:      "/fast",
		Timestamp: time.Now(),
	})
	if s.IsSlowloris(10 * time.Second) {
		t.Error("session avec requête ne doit pas être détectée comme Slowloris")
	}
}

func TestSessionStateIsSlowlorisNotYet(t *testing.T) {
	s := &correlation.SessionState{
		FirstSeen:    time.Now().Add(-5 * time.Second),
		LastActivity: time.Now(),
	}
	// Connexion ouverte depuis 5s seulement, seuil 10s → pas encore Slowloris
	if s.IsSlowloris(10 * time.Second) {
		t.Error("connexion de 5s ne doit pas être Slowloris avec seuil 10s")
	}
}

// ── Tests du Manager ──────────────────────────────────────────────────────

func TestManagerGetOrCreate(t *testing.T) {
	mgr := correlation.NewManager(500 * time.Millisecond)
	defer mgr.Close()

	key := correlation.SessionKey{SrcIP: [4]byte{10, 0, 0, 1}, SrcPort: 54321}

	s1 := mgr.GetOrCreate(key)
	s2 := mgr.GetOrCreate(key)

	if s1 != s2 {
		t.Error("GetOrCreate doit retourner la même session pour la même clé")
	}
}

func TestManagerGetOrCreateDifferentKeys(t *testing.T) {
	mgr := correlation.NewManager(500 * time.Millisecond)
	defer mgr.Close()

	key1 := correlation.SessionKey{SrcIP: [4]byte{10, 0, 0, 1}, SrcPort: 1000}
	key2 := correlation.SessionKey{SrcIP: [4]byte{10, 0, 0, 2}, SrcPort: 1000}

	s1 := mgr.GetOrCreate(key1)
	s2 := mgr.GetOrCreate(key2)

	if s1 == s2 {
		t.Error("GetOrCreate doit retourner des sessions différentes pour des clés différentes")
	}
}

func TestManagerUpdate(t *testing.T) {
	mgr := correlation.NewManager(500 * time.Millisecond)
	defer mgr.Close()

	key := correlation.SessionKey{SrcIP: [4]byte{192, 168, 1, 1}, SrcPort: 8080}

	mgr.Update(key, func(s *correlation.SessionState) {
		s.L3L4 = &correlation.L3L4{TTL: 64}
	})

	s := mgr.GetOrCreate(key)
	if s.L3L4 == nil {
		t.Fatal("L3L4 doit être défini après Update")
	}
	if s.L3L4.TTL != 64 {
		t.Errorf("TTL attendu 64, obtenu %d", s.L3L4.TTL)
	}
}

func TestManagerGCExpiresOldSessions(t *testing.T) {
	// Timeout très court pour que le GC puisse s'exécuter rapidement en test
	mgr := correlation.NewManager(50 * time.Millisecond)

	ctx, cancel := context.WithCancel(context.Background())
	mgr.StartGC(ctx)

	key := correlation.SessionKey{SrcIP: [4]byte{172, 16, 0, 1}, SrcPort: 9999}
	mgr.GetOrCreate(key)

	// Attendre que la session expire (timeout 50ms + marge)
	time.Sleep(300 * time.Millisecond)

	cancel()

	// La session doit avoir été envoyée dans ReadyCh ou déjà expirée
	// Ce test vérifie simplement l'absence de deadlock et de panic
	select {
	case <-mgr.ReadyCh:
		t.Log("session reçue dans ReadyCh — normal")
	default:
		t.Log("ReadyCh vide (session GC'd avant lecture) — acceptable")
	}
}

func TestManagerConcurrentAccess(t *testing.T) {
	// Test de charge concurrent : 100 goroutines écrivent simultanément
	mgr := correlation.NewManager(500 * time.Millisecond)
	ctx, cancel := context.WithCancel(context.Background())
	mgr.StartGC(ctx)
	defer func() {
		cancel()
		mgr.Close()
	}()

	done := make(chan struct{})
	for i := 0; i < 100; i++ {
		go func(i int) {
			key := correlation.SessionKey{
				SrcIP:   [4]byte{10, 0, byte(i >> 8), byte(i)},
				SrcPort: uint16(i + 1024),
			}
			mgr.Update(key, func(s *correlation.SessionState) {
				s.L3L4 = &correlation.L3L4{TTL: 64}
			})
			done <- struct{}{}
		}(i)
	}
	for i := 0; i < 100; i++ {
		<-done
	}
}
