package scanner

import (
	"context"
	"strings"
	"sync"
	"testing"
	"time"

	"aasd/internal/ai"
	"aasd/internal/mailer"
)

// newTestOrchestrator creates an orchestrator with mock dependencies for testing.
func newTestOrchestrator(t *testing.T) *Orchestrator {
	t.Helper()
	return NewOrchestrator(t.TempDir(), "https://test.local", nil, mailer.New(mailer.Config{}))
}

func TestNewOrchestrator(t *testing.T) {
	o := newTestOrchestrator(t)
	if o == nil {
		t.Fatal("expected non-nil orchestrator")
	}
	if o.results == nil {
		t.Error("expected results map to be initialized")
	}
	if len(o.results) != 0 {
		t.Errorf("expected empty results, got %d", len(o.results))
	}
}

func TestStartPipeline_Domain(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	result, err := o.StartPipeline(ctx, "example.com")
	if err != nil {
		t.Fatalf("StartPipeline failed: %v", err)
	}

	if result == nil {
		t.Fatal("expected non-nil result")
	}
	if result.Domain != "example.com" {
		t.Errorf("expected domain 'example.com', got '%s'", result.Domain)
	}
	if result.ReportToken == "" {
		t.Error("expected non-empty token")
	}
	if result.Status != StatusPending {
		t.Errorf("expected status 'pending', got '%s'", result.Status)
	}
	if result.CreatedAt.IsZero() {
		t.Error("expected CreatedAt to be set")
	}
}

func TestStartPipeline_IP(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	result, err := o.StartPipeline(ctx, "192.168.1.1")
	if err != nil {
		t.Fatalf("StartPipeline failed: %v", err)
	}

	if result.Domain != "192.168.1.1" {
		t.Errorf("expected domain '192.168.1.1', got '%s'", result.Domain)
	}
	// For IPs, SelectedDomain should be set to the IP
	if result.SelectedDomain != "192.168.1.1" {
		t.Errorf("expected SelectedDomain '192.168.1.1', got '%s'", result.SelectedDomain)
	}
}

func TestGetResult_Found(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	result, err := o.StartPipeline(ctx, "example.com")
	if err != nil {
		t.Fatal(err)
	}

	got, exists := o.GetResult(result.ReportToken)
	if !exists {
		t.Fatal("expected result to exist")
	}
	if got.ReportToken != result.ReportToken {
		t.Errorf("expected token '%s', got '%s'", result.ReportToken, got.ReportToken)
	}
}

func TestGetResult_NotFound(t *testing.T) {
	o := newTestOrchestrator(t)
	_, exists := o.GetResult("nonexistent-token")
	if exists {
		t.Error("expected false for nonexistent token")
	}
}

func TestGetResultByDomain_Found(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	_, err := o.StartPipeline(ctx, "example.com")
	if err != nil {
		t.Fatal(err)
	}

	result := o.GetResultByDomain("example.com")
	if result == nil {
		t.Fatal("expected non-nil result")
	}
	if result.Domain != "example.com" {
		t.Errorf("expected domain 'example.com', got '%s'", result.Domain)
	}
}

func TestGetResultByDomain_NotFound(t *testing.T) {
	o := newTestOrchestrator(t)
	result := o.GetResultByDomain("nonexistent.com")
	if result != nil {
		t.Error("expected nil for nonexistent domain")
	}
}

func TestGetAllResults_Empty(t *testing.T) {
	o := newTestOrchestrator(t)
	results := o.GetAllResults()
	if len(results) != 0 {
		t.Errorf("expected 0 results, got %d", len(results))
	}
}

func TestGetAllResults_Multiple(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	o.StartPipeline(ctx, "example.com")
	o.StartPipeline(ctx, "test.org")

	results := o.GetAllResults()
	if len(results) != 2 {
		t.Errorf("expected 2 results, got %d", len(results))
	}
}

func TestCountStats_Empty(t *testing.T) {
	o := newTestOrchestrator(t)
	total, completed, running := o.CountStats()
	if total != 0 {
		t.Errorf("expected total 0, got %d", total)
	}
	if completed != 0 {
		t.Errorf("expected completed 0, got %d", completed)
	}
	if running != 0 {
		t.Errorf("expected running 0, got %d", running)
	}
}

func TestCountStats_Mixed(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	r1, _ := o.StartPipeline(ctx, "example.com")
	r2, _ := o.StartPipeline(ctx, "test.org")

	// Manually set statuses
	o.updateStatus(r1.ReportToken, StatusCompleted)
	o.updateStatus(r2.ReportToken, StatusScanning)

	total, completed, running := o.CountStats()
	if total != 2 {
		t.Errorf("expected total 2, got %d", total)
	}
	if completed != 1 {
		t.Errorf("expected completed 1, got %d", completed)
	}
	if running != 1 {
		t.Errorf("expected running 1, got %d", running)
	}
}

func TestUpdateStatus(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	result, _ := o.StartPipeline(ctx, "example.com")

	o.updateStatus(result.ReportToken, StatusDiscovering)
	got, _ := o.GetResult(result.ReportToken)
	if got.Status != StatusDiscovering {
		t.Errorf("expected 'discovering', got '%s'", got.Status)
	}

	o.updateStatus(result.ReportToken, StatusCompleted)
	got, _ = o.GetResult(result.ReportToken)
	if got.Status != StatusCompleted {
		t.Errorf("expected 'completed', got '%s'", got.Status)
	}
}

func TestSelectAndScan_WrongStatus(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	result, _ := o.StartPipeline(ctx, "example.com")

	// status is "pending" — selection should fail
	err := o.SelectAndScan(ctx, result.ReportToken, "www.example.com")
	if err == nil {
		t.Fatal("expected error for SelectAndScan with pending status")
	}
	if !strings.Contains(err.Error(), "cannot select target") {
		t.Errorf("expected error to contain 'cannot select target', got: %v", err)
	}
}

func TestSelectAndScan_NonexistentToken(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	err := o.SelectAndScan(ctx, "nonexistent", "www.example.com")
	if err == nil {
		t.Fatal("expected error for nonexistent token")
	}
}

func TestSelectAndScan_ValidTransition(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	result, _ := o.StartPipeline(ctx, "example.com")
	// Manually set to awaiting_selection (as discoverSubdomains would)
	o.updateStatus(result.ReportToken, StatusAwaitingSelection)

	err := o.SelectAndScan(ctx, result.ReportToken, "www.example.com")
	if err != nil {
		t.Fatalf("SelectAndScan failed: %v", err)
	}

	got, _ := o.GetResult(result.ReportToken)
	if got.SelectedDomain != "www.example.com" {
		t.Errorf("expected SelectedDomain 'www.example.com', got '%s'", got.SelectedDomain)
	}
	// Status should still be awaiting_selection (scan runs in a goroutine)
	if got.Status != StatusAwaitingSelection {
		t.Errorf("expected status to remain 'awaiting_selection', got '%s'", got.Status)
	}
}

func TestGetResult_ReturnsCopy(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	result, _ := o.StartPipeline(ctx, "example.com")

	// Get a copy and modify it
	got1, _ := o.GetResult(result.ReportToken)
	got1.Status = StatusCompleted

	// Get another copy — should be the original value
	got2, _ := o.GetResult(result.ReportToken)
	if got2.Status != StatusPending {
		t.Errorf("expected original status 'pending', got '%s' (modification leaked)", got2.Status)
	}
}

func TestConcurrentAccess(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()
	var wg sync.WaitGroup

	// Start multiple pipelines concurrently
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			domain := "test" + string(rune('0'+id)) + ".com"
			_, err := o.StartPipeline(ctx, domain)
			if err != nil {
				t.Errorf("StartPipeline failed for %s: %v", domain, err)
			}
		}(i)
	}
	wg.Wait()

	// Verify all 10 results exist
	results := o.GetAllResults()
	if len(results) != 10 {
		t.Errorf("expected 10 results, got %d", len(results))
	}

	// Concurrent reads
	var readWg sync.WaitGroup
	for i := 0; i < 20; i++ {
		readWg.Add(1)
		go func() {
			defer readWg.Done()
			o.CountStats()
			o.GetAllResults()
		}()
	}
	readWg.Wait()
}

func TestSendEmailReport_NotFound(t *testing.T) {
	o := newTestOrchestrator(t)
	err := o.SendEmailReport("nonexistent", "user@example.com")
	if err == nil {
		t.Fatal("expected error for nonexistent token")
	}
}

func TestSendEmailReport_NotCompleted(t *testing.T) {
	o := newTestOrchestrator(t)
	ctx := context.Background()

	result, _ := o.StartPipeline(ctx, "example.com")
	err := o.SendEmailReport(result.ReportToken, "user@example.com")
	if err == nil {
		t.Fatal("expected error for non-completed scan")
	}
}

func TestNewOrchestrator_NilDeps(t *testing.T) {
	// Should handle nil AI client and nil mailer gracefully
	o := NewOrchestrator("/tmp", "https://test.local", (*ai.Client)(nil), (*mailer.Mailer)(nil))
	if o == nil {
		t.Fatal("expected non-nil orchestrator")
	}
}

func TestStatusConstants_AreStrings(t *testing.T) {
	if StatusPending != "pending" {
		t.Errorf("StatusPending should be 'pending', got '%s'", StatusPending)
	}
	if StatusDiscovering != "discovering" {
		t.Errorf("StatusDiscovering should be 'discovering', got '%s'", StatusDiscovering)
	}
	if StatusAwaitingSelection != "awaiting_selection" {
		t.Errorf("StatusAwaitingSelection should be 'awaiting_selection', got '%s'", StatusAwaitingSelection)
	}
	if StatusScanning != "scanning" {
		t.Errorf("StatusScanning should be 'scanning', got '%s'", StatusScanning)
	}
	if StatusGenerating != "generating" {
		t.Errorf("StatusGenerating should be 'generating', got '%s'", StatusGenerating)
	}
	if StatusCompleted != "completed" {
		t.Errorf("StatusCompleted should be 'completed', got '%s'", StatusCompleted)
	}
	if StatusFailed != "failed" {
		t.Errorf("StatusFailed should be 'failed', got '%s'", StatusFailed)
	}
}

func TestOrchestrator_RaceFreeStatusTransition(t *testing.T) {
	// Verify the race condition guard: discoverSubdomains must not overwrite
	// a status that was already changed by SelectAndScan
	o := newTestOrchestrator(t)
	ctx := context.Background()

	result, _ := o.StartPipeline(ctx, "example.com")

	// Simulate: discovery finishes and SelectAndScan is called concurrently
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		// discoverSubdomains logic: set discovering → then set awaiting_selection
		// but guarded by checking current status is still discovering
		o.updateStatus(result.ReportToken, StatusDiscovering)
		time.Sleep(10 * time.Millisecond)

		o.mu.Lock()
		if o.results[result.ReportToken].Status == StatusDiscovering {
			o.results[result.ReportToken].Status = StatusAwaitingSelection
		}
		o.mu.Unlock()
	}()

	go func() {
		defer wg.Done()
		time.Sleep(5 * time.Millisecond)
		// SelectAndScan logic: only works if status is awaiting_selection
		o.mu.Lock()
		if r, ok := o.results[result.ReportToken]; ok && r.Status == StatusAwaitingSelection {
			r.SelectedDomain = "www.example.com"
		}
		o.mu.Unlock()
	}()

	wg.Wait()

	got, _ := o.GetResult(result.ReportToken)
	// The status should NOT be discovering — either awaiting_selection or scanning
	if got.Status == StatusDiscovering {
		t.Error("status should not remain 'discovering' after both goroutines complete")
	}
}
