package routing

import (
	"context"
	"testing"

	"github.com/go-redis/redis/v8"
	"trip-planner/internal/cache"
)

// TestCacheAsideSearch tests the cache-aside pattern for search results.
// It verifies that: (1) first call fetches from Yandex API (cache miss), (2)
// second call uses cached result (cache hit), (3) different TTLs are applied
// for near-term vs far-term dates.
func TestCacheAsideSearch(t *testing.T) {
	ctx := context.Background()

	fetchCallCount := 0
	fetchFunc := func() ([]byte, error) {
		fetchCallCount++
		return []byte(`{"legs":[{"from":{"name":"Moscow"},"to":{"name":"Tula"},"duration":3600,"transport":"train","is_transfer":false}]}`), nil
	}

	// First call: cache miss, should fetch from backend
	searchKey := cache.GetSearchKey("c146", "c213", "2026-08-15-test1")
	store := cache.NewCacheStore(redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 1}))
	data, err := cache.NewCacheAside(store).GetSearch(ctx, searchKey, fetchFunc, false)
	if err != nil {
		t.Fatalf("expected no error on cache miss, got: %v", err)
	}
	if string(data) != `{"legs":[{"from":{"name":"Moscow"},"to":{"name":"Tula"},"duration":3600,"transport":"train","is_transfer":false}]}` {
		t.Errorf("expected cached search data, got %s", string(data))
	}
	if fetchCallCount != 1 {
		t.Errorf("expected 1 fetch call, got %d", fetchCallCount)
	}

	// Second call: cache hit, should not fetch from backend
	fetchCallCount = 0
	data, err = cache.NewCacheAside(store).GetSearch(ctx, searchKey, fetchFunc, false)
	if err != nil {
		t.Fatalf("expected no error on cache hit, got: %v", err)
	}
	if fetchCallCount != 0 {
		t.Errorf("expected 0 fetch calls on cache hit, got %d", fetchCallCount)
	}
}

// TestCacheAsideSearchFarTerm tests cache-aside search with far-term TTL.
func TestCacheAsideSearchFarTerm(t *testing.T) {
	ctx := context.Background()

	fetchCallCount := 0
	fetchFunc := func() ([]byte, error) {
		fetchCallCount++
		return []byte(`{"legs":[]}`), nil
	}

	// Far-term search key - should use SearchFarTermTTL (7 days)
	store := cache.NewCacheStore(redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 1}))
	farKey := &cache.CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-09-15-test2"}
	data, err := cache.NewCacheAside(store).GetSearch(ctx, farKey, fetchFunc, true)
	if err != nil {
		t.Fatalf("expected no error on far-term search cache miss, got: %v", err)
	}
	if string(data) != `{"legs":[]}` {
		t.Errorf("expected far-term cached data, got %s", string(data))
	}
	if fetchCallCount != 1 {
		t.Errorf("expected 1 fetch call for far-term, got %d", fetchCallCount)
	}
}

// TestCacheAsideSearchNearTerm tests cache-aside search with near-term TTL.
func TestCacheAsideSearchNearTerm(t *testing.T) {
	ctx := context.Background()

	fetchCallCount := 0
	fetchFunc := func() ([]byte, error) {
		fetchCallCount++
		return []byte(`{"legs":[]}`), nil
	}

	// Near-term search key - should use SearchNearTermTTL (3 hours)
	store := cache.NewCacheStore(redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 1}))
	nearKey := &cache.CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-15-test3"}
	data, err := cache.NewCacheAside(store).GetSearch(ctx, nearKey, fetchFunc, false)
	if err != nil {
		t.Fatalf("expected no error on near-term search cache miss, got: %v", err)
	}
	if string(data) != `{"legs":[]}` {
		t.Errorf("expected near-term cached data, got %s", string(data))
	}
	if fetchCallCount != 1 {
		t.Errorf("expected 1 fetch call for near-term, got %d", fetchCallCount)
	}
}

// TestRouteParetoRanking tests that FindRoutesPareto correctly returns
// Pareto-optimal routes (non-dominated) based on time, transfers, and cost.
// A route is dominated if another route is better or equal in all metrics.
func TestRouteParetoRanking(t *testing.T) {
	graph := NewGraph()

	// Add stations along a route
	graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
	graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
	graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
	graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"})

	// Direct route: Moscow → Kursk (0 transfers, 3600s, cost 0)
	graph.AddEdge(&Edge{
		From:       graph.Nodes()[0], // s1 Moscow
		To:         graph.Nodes()[3], // s4 Kursk
		Kind:       EdgeKindReal,
		Duration:   3600,
		Transport:  "train",
		IsTransfer: false,
		Cost:       0,
	})

	// Indirect route: Moscow → Tula → Vladimir → Kursk (3 transfers, 3*3600=10800s, cost 0)
	graph.AddEdge(&Edge{
		From:       graph.Nodes()[0], // s1 Moscow
		To:         graph.Nodes()[1], // s2 Tula
		Kind:       EdgeKindReal,
		Duration:   3600,
		Transport:  "train",
		IsTransfer: false,
		Cost:       0,
	})
	graph.AddEdge(&Edge{
		From:       graph.Nodes()[1], // s2 Tula
		To:         graph.Nodes()[2], // s3 Vladimir
		Kind:       EdgeKindReal,
		Duration:   3600,
		Transport:  "train",
		IsTransfer: false,
		Cost:       0,
	})
	graph.AddEdge(&Edge{
		From:       graph.Nodes()[2], // s3 Vladimir
		To:         graph.Nodes()[3], // s4 Kursk
		Kind:       EdgeKindReal,
		Duration:   3600,
		Transport:  "train",
		IsTransfer: false,
		Cost:       0,
	})

	// Fast but expensive route: Moscow → Tula (1 leg, 1800s, cost 5000)
	// This would be an alternative direct route with higher cost but lower duration
	// Add a second direct edge with different characteristics if needed

	opts := SearchOptions{MaxTransfers: 3, MCT: 300}
	results := graph.FindRoutesPareto("s1", "s4", opts)

	// Should find at least the direct route (0 transfers, 3600s)
	if len(results) == 0 {
		t.Error("expected at least 1 Pareto-optimal route")
	}

	// The direct route (0 transfers, 3600s) should be Pareto-optimal
	// since no other route has both fewer transfers and less duration
	directFound := false
	for _, r := range results {
		if r.TotalDuration == 3600 && r.TotalTransfers == 0 {
			directFound = true
			break
		}
	}
	if !directFound {
		t.Error("expected direct route (0 transfers, 3600s) in Pareto results")
	}

	// Test with routes that have different cost values
	graph2 := NewGraph()
	graph2.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
	graph2.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
	graph2.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
	graph2.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"})

	// Route A: 0 transfers, 3600s, cost 1000
	graph2.AddEdge(&Edge{
		From:       graph2.Nodes()[0],
		To:         graph2.Nodes()[3],
		Kind:       EdgeKindReal,
		Duration:   3600,
		Transport:  "train",
		IsTransfer: false,
		Cost:       1000,
	})

	// Route B: 0 transfers, 4000s, cost 0 (cheaper but slower)
	// This route should NOT dominate Route A (different cost), and Route A
	// should NOT dominate Route B (Route A is faster but more expensive)
	graph2.AddEdge(&Edge{
		From:       graph2.Nodes()[0],
		To:         graph2.Nodes()[3],
		Kind:       EdgeKindReal,
		Duration:   4000,
		Transport:  "train",
		IsTransfer: false,
		Cost:       0,
	})

	// Route C: 1 transfer, 3000s, cost 0 (middle ground)
	graph2.AddEdge(&Edge{
		From:       graph2.Nodes()[0],
		To:         graph2.Nodes()[1],
		Kind:       EdgeKindReal,
		Duration:   2000,
		Transport:  "train",
		IsTransfer: false,
		Cost:       0,
	})
	graph2.AddEdge(&Edge{
		From:       graph2.Nodes()[1],
		To:         graph2.Nodes()[3],
		Kind:       EdgeKindReal,
		Duration:   1000,
		Transport:  "train",
		IsTransfer: true,
		Cost:       0,
	})

	opts2 := SearchOptions{MaxTransfers: 3, MCT: 300}
	results2 := graph2.FindRoutesPareto("s1", "s4", opts2)

	// Should find at least some routes
	if len(results2) == 0 {
		t.Error("expected at least 1 Pareto-optimal route with cost variation")
	}

	// Verify no route is dominated by another in all metrics
	for i, r1 := range results2 {
		for j, r2 := range results2 {
			if i == j {
				continue
			}
			// Check if r2 dominates r1
			r2DominatesR1 := r2.TotalDuration <= r1.TotalDuration &&
				r2.TotalTransfers <= r1.TotalTransfers &&
				r2.Cost <= r1.Cost &&
				(r2.TotalDuration < r1.TotalDuration ||
					r2.TotalTransfers < r1.TotalTransfers ||
					r2.Cost < r1.Cost)
			if r2DominatesR1 {
				t.Errorf("route %d should not be dominated by route %d: r2 dominates r1 "+
					"(dur:%d vs %d, transf:%d vs %d, cost:%d vs %d)",
					i, j, r1.TotalDuration, r2.TotalDuration,
					r1.TotalTransfers, r2.TotalTransfers,
					r1.Cost, r2.Cost)
			}
		}
	}
}