package search

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/ashie/gosearch/internal/contracts"
	"github.com/ashie/gosearch/internal/engines"
)

// mockEngine is a test engine that returns a predefined response or error.
type mockEngine struct {
	name    string
	resp    contracts.SearchResponse
	err     error
	delay   time.Duration
}

func (m *mockEngine) Name() string { return m.name }
func (m *mockEngine) Search(ctx context.Context, req contracts.SearchRequest) (contracts.SearchResponse, error) {
	if m.delay > 0 {
		select {
		case <-ctx.Done():
			return contracts.SearchResponse{}, ctx.Err()
		case <-time.After(m.delay):
		}
	}
	if m.err != nil {
		return contracts.SearchResponse{}, m.err
	}
	return m.resp, nil
}

func TestSearch_ConcurrentEngines(t *testing.T) {
	// Two engines that each take 100ms. If sequential, total would be ~200ms.
	// Concurrent should be ~100ms.
	registry := map[string]engines.Engine{
		"fast-a": &mockEngine{
			name: "fast-a",
			resp: contracts.SearchResponse{
				Query:           "test",
				NumberOfResults: 1,
				Results: []contracts.MainResult{
					{Title: "Result A", Engine: "fast-a"},
				},
				Answers:            []map[string]any{},
				Corrections:        []string{},
				Infoboxes:          []map[string]any{},
				Suggestions:        []string{},
				UnresponsiveEngines: [][2]string{},
			},
			delay: 100 * time.Millisecond,
		},
		"fast-b": &mockEngine{
			name: "fast-b",
			resp: contracts.SearchResponse{
				Query:           "test",
				NumberOfResults: 1,
				Results: []contracts.MainResult{
					{Title: "Result B", Engine: "fast-b"},
				},
				Answers:            []map[string]any{},
				Corrections:        []string{},
				Infoboxes:          []map[string]any{},
				Suggestions:        []string{},
				UnresponsiveEngines: [][2]string{},
			},
			delay: 100 * time.Millisecond,
		},
	}

	svc := &Service{
		upstreamClient: nil,
		planner:        engines.NewPlanner([]string{"fast-a", "fast-b"}),
		localEngines:  registry,
	}

	req := SearchRequest{
		Format:  FormatJSON,
		Query:   "test",
		Engines: []string{"fast-a", "fast-b"},
	}

	start := time.Now()
	resp, err := svc.Search(context.Background(), req)
	elapsed := time.Since(start)

	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(resp.Results) != 2 {
		t.Errorf("expected 2 results, got %d", len(resp.Results))
	}
	// Should complete in well under 200ms (sequential time).
	if elapsed > 180*time.Millisecond {
		t.Errorf("engines not running concurrently: took %v", elapsed)
	}
}

func TestSearch_GracefulDegradation(t *testing.T) {
	// One engine succeeds, one fails. Both should be represented:
	// the failing engine in unresponsive_engines, the succeeding one in results.
	registry := map[string]engines.Engine{
		"good": &mockEngine{
			name: "good",
			resp: contracts.SearchResponse{
				Query:           "test",
				NumberOfResults: 1,
				Results: []contracts.MainResult{
					{Title: "Good Result", Engine: "good"},
				},
				Answers:            []map[string]any{},
				Corrections:        []string{},
				Infoboxes:          []map[string]any{},
				Suggestions:        []string{},
				UnresponsiveEngines: [][2]string{},
			},
		},
		"bad": &mockEngine{
			name: "bad",
			err:  errors.New("connection refused"),
		},
	}

	svc := &Service{
		upstreamClient: nil,
		planner:        engines.NewPlanner([]string{"good", "bad"}),
		localEngines:  registry,
	}

	req := SearchRequest{
		Format:  FormatJSON,
		Query:   "test",
		Engines: []string{"good", "bad"},
	}

	resp, err := svc.Search(context.Background(), req)
	if err != nil {
		t.Fatalf("search should not fail on individual engine error: %v", err)
	}

	// Should still have the good result.
	if len(resp.Results) != 1 {
		t.Errorf("expected 1 result from good engine, got %d", len(resp.Results))
	}

	// The bad engine should appear in unresponsive_engines.
	foundBad := false
	for _, ue := range resp.UnresponsiveEngines {
		if ue[0] == "bad" {
			foundBad = true
		}
	}
	if !foundBad {
		t.Errorf("expected 'bad' in unresponsive_engines, got %v", resp.UnresponsiveEngines)
	}
}

func TestSearch_AllEnginesFail(t *testing.T) {
	registry := map[string]engines.Engine{
		"failing": &mockEngine{
			name: "failing",
			err:  errors.New("timeout"),
		},
	}

	svc := &Service{
		upstreamClient: nil,
		planner:        engines.NewPlanner([]string{"failing"}),
		localEngines:  registry,
	}

	req := SearchRequest{
		Format:  FormatJSON,
		Query:   "test",
		Engines: []string{"failing"},
	}

	resp, err := svc.Search(context.Background(), req)
	if err != nil {
		t.Fatalf("should not return error even when all engines fail: %v", err)
	}

	if len(resp.Results) != 0 {
		t.Errorf("expected 0 results, got %d", len(resp.Results))
	}
	if len(resp.UnresponsiveEngines) != 1 {
		t.Errorf("expected 1 unresponsive engine, got %d", len(resp.UnresponsiveEngines))
	}
}

func TestSearch_ContextCancellation(t *testing.T) {
	slowEngine := &mockEngine{
		name:  "slow",
		delay: 5 * time.Second,
		resp: contracts.SearchResponse{
			Query:               "test",
			Results:             []contracts.MainResult{},
			Answers:             []map[string]any{},
			Corrections:         []string{},
			Infoboxes:           []map[string]any{},
			Suggestions:         []string{},
			UnresponsiveEngines: [][2]string{},
		},
	}

	svc := &Service{
		upstreamClient: nil,
		planner:        engines.NewPlanner([]string{"slow"}),
		localEngines: map[string]engines.Engine{
			"slow": slowEngine,
		},
	}

	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()

	req := SearchRequest{
		Format:  FormatJSON,
		Query:   "test",
		Engines: []string{"slow"},
	}

	start := time.Now()
	resp, err := svc.Search(ctx, req)
	elapsed := time.Since(start)

	// Should not error — graceful degradation handles context cancellation.
	if err != nil {
		t.Fatalf("should not error on context cancel: %v", err)
	}

	if len(resp.Results) != 0 {
		t.Errorf("expected 0 results from cancelled engine, got %d", len(resp.Results))
	}

	// Should complete quickly, not wait for the 5s delay.
	if elapsed > 200*time.Millisecond {
		t.Errorf("context cancellation not respected: took %v", elapsed)
	}

	// Engine should be marked unresponsive.
	if len(resp.UnresponsiveEngines) != 1 {
		t.Errorf("expected 1 unresponsive engine, got %d", len(resp.UnresponsiveEngines))
	}
}
