package heuristics

import (
	"testing"
	"time"

	"github.com/breakpilot/pca-platform/heuristic-service/internal/config"
)

func TestNewScorer(t *testing.T) {
	cfg := config.DefaultConfig()
	scorer := NewScorer(cfg)

	if scorer == nil {
		t.Fatal("Expected non-nil scorer")
	}
	if scorer.config == nil {
		t.Error("Expected config to be set")
	}
	if scorer.sessions == nil {
		t.Error("Expected sessions map to be initialized")
	}
}

func TestGetOrCreateSession(t *testing.T) {
	cfg := config.DefaultConfig()
	scorer := NewScorer(cfg)

	// First call should create session
	session1 := scorer.GetOrCreateSession("test-session-1")
	if session1 == nil {
		t.Fatal("Expected non-nil session")
	}
	if session1.SessionID != "test-session-1" {
		t.Errorf("Expected session ID 'test-session-1', got '%s'", session1.SessionID)
	}

	// Second call should return same session
	session2 := scorer.GetOrCreateSession("test-session-1")
	if session1 != session2 {
		t.Error("Expected same session instance on second call")
	}

	// Different ID should create new session
	session3 := scorer.GetOrCreateSession("test-session-2")
	if session1 == session3 {
		t.Error("Expected different session for different ID")
	}
}

func TestCalculateScore_NewSession(t *testing.T) {
	cfg := config.DefaultConfig()
	scorer := NewScorer(cfg)

	// New session with no activity should have low score
	scorer.GetOrCreateSession("test-new")
	score := scorer.CalculateScore("test-new")

	if score < 0 || score > 1 {
		t.Errorf("Expected score between 0 and 1, got %f", score)
	}
}

func TestCalculateScore_HighActivity(t *testing.T) {
	cfg := config.DefaultConfig()
	scorer := NewScorer(cfg)

	session := scorer.GetOrCreateSession("test-active")
	session.StartTime = time.Now().Add(-30 * time.Second)
	session.VisibleTime = 28.0 // High visibility
	session.MaxScrollPercent = 0.8
	session.ClickCount = 10
	session.MouseMoves = 100
	session.MouseVelocities = []float64{100, 150, 80, 200, 120, 90}
	session.ClickIntervals = []float64{1.5, 2.0, 1.2, 0.8}

	score := scorer.CalculateScore("test-active")

	// Active session should have higher score
	if score < 0.5 {
		t.Errorf("Expected score > 0.5 for active session, got %f", score)
	}
}

func TestCalculateScore_BotLikeActivity(t *testing.T) {
	cfg := config.DefaultConfig()
	scorer := NewScorer(cfg)

	session := scorer.GetOrCreateSession("test-bot")
	session.StartTime = time.Now().Add(-5 * time.Second)
	session.VisibleTime = 1.0 // Very short
	session.MaxScrollPercent = 0.0
	session.ClickCount = 0
	session.MouseMoves = 0

	score := scorer.CalculateScore("test-bot")

	// Bot-like session should have very low score
	if score > 0.3 {
		t.Errorf("Expected score < 0.3 for bot-like session, got %f", score)
	}
}

func TestCalculateScore_UniformMouseMovement(t *testing.T) {
	cfg := config.DefaultConfig()
	scorer := NewScorer(cfg)

	session := scorer.GetOrCreateSession("test-uniform")
	session.StartTime = time.Now().Add(-20 * time.Second)
	session.VisibleTime = 18.0
	session.MouseMoves = 50
	// Very uniform velocities (suspicious)
	session.MouseVelocities = []float64{100, 100, 100, 100, 100, 100, 100, 100}

	score := scorer.CalculateScore("test-uniform")

	// Uniform movement should result in lower pointer score
	if score > 0.7 {
		t.Errorf("Expected score < 0.7 for uniform mouse movement, got %f", score)
	}
}

func TestEvaluateRequest(t *testing.T) {
	cfg := config.DefaultConfig()
	scorer := NewScorer(cfg)

	// High score session
	session := scorer.GetOrCreateSession("test-evaluate")
	session.StartTime = time.Now().Add(-60 * time.Second)
	session.VisibleTime = 55.0
	session.MaxScrollPercent = 0.9
	session.ClickCount = 15
	session.MouseMoves = 200
	session.MouseVelocities = []float64{100, 150, 80, 200, 120, 90, 110}

	result := scorer.EvaluateRequest("test-evaluate", "/default", nil)

	if result.SessionID != "test-evaluate" {
		t.Errorf("Expected session ID 'test-evaluate', got '%s'", result.SessionID)
	}
	if result.Action != "allow" && result.Score >= cfg.Thresholds.ScorePass {
		t.Errorf("Expected 'allow' action for high score, got '%s'", result.Action)
	}
}

func TestEvaluateRequest_Challenge(t *testing.T) {
	cfg := config.DefaultConfig()
	scorer := NewScorer(cfg)

	// Low score session
	scorer.GetOrCreateSession("test-challenge")

	result := scorer.EvaluateRequest("test-challenge", "/api", nil)

	if result.Action != "challenge" {
		t.Errorf("Expected 'challenge' action for new session, got '%s'", result.Action)
	}
	if result.StepUpMethod == "" {
		t.Error("Expected step-up method to be set for challenge")
	}
}

func TestCleanupOldSessions(t *testing.T) {
	cfg := config.DefaultConfig()
	scorer := NewScorer(cfg)

	// Create some sessions
	scorer.GetOrCreateSession("session-new")

	oldSession := scorer.GetOrCreateSession("session-old")
	oldSession.StartTime = time.Now().Add(-2 * time.Hour)

	// Verify both exist
	if len(scorer.sessions) != 2 {
		t.Errorf("Expected 2 sessions, got %d", len(scorer.sessions))
	}

	// Cleanup with 1 hour max age
	scorer.CleanupOldSessions(1 * time.Hour)

	// Old session should be removed
	if len(scorer.sessions) != 1 {
		t.Errorf("Expected 1 session after cleanup, got %d", len(scorer.sessions))
	}

	if _, exists := scorer.sessions["session-old"]; exists {
		t.Error("Expected old session to be cleaned up")
	}
}

func TestCalculateVariance(t *testing.T) {
	tests := []struct {
		name     string
		values   []float64
		expected float64
	}{
		{
			name:     "empty",
			values:   []float64{},
			expected: 0.0,
		},
		{
			name:     "single value",
			values:   []float64{5.0},
			expected: 0.0,
		},
		{
			name:     "uniform values",
			values:   []float64{5.0, 5.0, 5.0, 5.0},
			expected: 0.0,
		},
		{
			name:     "varied values",
			values:   []float64{1.0, 2.0, 3.0, 4.0, 5.0},
			expected: 2.5, // Variance of [1,2,3,4,5]
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := calculateVariance(tt.values)
			if tt.expected == 0.0 && result != 0.0 {
				t.Errorf("Expected 0 variance, got %f", result)
			}
			if tt.expected != 0.0 && (result < tt.expected-0.1 || result > tt.expected+0.1) {
				t.Errorf("Expected variance ~%f, got %f", tt.expected, result)
			}
		})
	}
}

func TestSigmoid(t *testing.T) {
	// Test sigmoid at midpoint
	result := sigmoid(0.5, 0.5, 10)
	if result < 0.49 || result > 0.51 {
		t.Errorf("Expected sigmoid(0.5, 0.5, 10) ~ 0.5, got %f", result)
	}

	// Test sigmoid well above midpoint
	result = sigmoid(1.0, 0.5, 10)
	if result < 0.9 {
		t.Errorf("Expected sigmoid(1.0, 0.5, 10) > 0.9, got %f", result)
	}

	// Test sigmoid well below midpoint
	result = sigmoid(0.0, 0.5, 10)
	if result > 0.1 {
		t.Errorf("Expected sigmoid(0.0, 0.5, 10) < 0.1, got %f", result)
	}
}
