package main

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/go-redis/redis/v8"

	"trip-planner/internal/metrics"
	"trip-planner/internal/airports"
	"trip-planner/internal/routing"
	"trip-planner/internal/storage"
	"trip-planner/internal/yandex"
)

func flushRedisForTest(t *testing.T, client *redis.Client) {
	// Clear preference-related keys from Redis to ensure test isolation
	// The keyString sanitization prepends "unknown:" and replaces ":" with "_colon_"
	// Actual keys look like: "unknown:prefs_colon_saved_city_colon_testuser1"
	keys, err := client.Keys(context.Background(), "unknown:prefs*").Result()
	if err != nil {
		t.Logf("warning: could not flush preference keys: %v", err)
		return
	}
	for _, key := range keys {
		if err := client.Del(context.Background(), key).Err(); err != nil {
			t.Logf("warning: could not delete key %s: %v", key, err)
		}
	}
	// Also delete search_history keys
	keys2, err := client.Keys(context.Background(), "unknown:search_history*").Result()
	if err != nil {
		t.Logf("warning: could not flush search history keys: %v", err)
		return
	}
	for _, key := range keys2 {
		if err := client.Del(context.Background(), key).Err(); err != nil {
			t.Logf("warning: could not delete key %s: %v", key, err)
		}
	}
}

func newMockHandlerContext() *HandlerContext {
	redisClient := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})

	// Create an empty routing graph
	router := routing.NewGraph()

	// Create Yandex client
	yandexClient := yandex.NewClient("test-key")

	return NewHandlerContext(redisClient, router, yandexClient, metrics.New())
}

func TestHandlerCityAutocomplete(t *testing.T) {
	h := newMockHandlerContext()
	req := httptest.NewRequest("GET", "/v1/cities?query=mos", nil)
	rr := httptest.NewRecorder()
	CityAutocomplete(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp []cityResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}
	t.Logf("city autocomplete response: %d cities", len(resp))
}

func TestHandlerCityStations(t *testing.T) {
	h := newMockHandlerContext()
	req := httptest.NewRequest("GET", "/v1/cities/1/stations", nil)
	rr := httptest.NewRecorder()
	CityStations(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}
}

func TestHandlerCityStationsWithNeighbors(t *testing.T) {
	h := newMockHandlerContext()

	// Test: City 1 with closed station should include neighbors
	req := httptest.NewRequest("GET", "/v1/cities/1/stations", nil)
	rr := httptest.NewRecorder()
	CityStations(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp cityStationResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	// Should have stations
	if len(resp.Stations) == 0 {
		t.Error("expected at least one station")
	}
	t.Logf("City 1 stations: %+v", resp.Stations)

	// Should have neighbors when station is closed (city 1 has closed stations)
	if len(resp.Neighbors) == 0 {
		t.Error("expected neighboring stations for closed main station")
	}
	t.Logf("City 1 neighbors: %+v", resp.Neighbors)

	// Verify neighbor has source field
	for _, n := range resp.Neighbors {
		if n.Source != "manual" && n.Source != "geo" {
			t.Errorf("expected neighbor source to be 'manual' or 'geo', got %s", n.Source)
		}
		t.Logf("  Neighbor: %s (source=%s, isExcluded=%v)", n.Name, n.Source, n.IsExcluded)
	}
}

func TestHandlerRouteSearch(t *testing.T) {
	h := newMockHandlerContext()

	// Add nodes and edges to the graph to test route finding
	graph := routing.NewGraph()
	graph.AddNode(&routing.Node{ID: "c146", Type: routing.NodeTypeCity, Name: "Simferopol"})
	graph.AddNode(&routing.Node{ID: "c213", Type: routing.NodeTypeCity, Name: "Moscow"})
	graph.AddNode(&routing.Node{ID: "s9600213", Type: routing.NodeTypeStation, Name: "Шереметьево", CityCode: "c146"})
	graph.AddNode(&routing.Node{ID: "s9600396", Type: routing.NodeTypeStation, Name: "Симферополь", CityCode: "c146"})

	// Add synthetic edges: station <-> city
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[2], // s9600213
		To:         graph.Nodes()[0], // c146
		Kind:       routing.EdgeKindSynthetic,
		Duration:   300,
		Transport:  "train",
		IsTransfer: true,
	})
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[0], // c146
		To:         graph.Nodes()[2], // s9600213
		Kind:       routing.EdgeKindSynthetic,
		Duration:   300,
		Transport:  "train",
		IsTransfer: true,
	})
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[3], // s9600396
		To:         graph.Nodes()[0], // c146
		Kind:       routing.EdgeKindSynthetic,
		Duration:   300,
		Transport:  "train",
		IsTransfer: true,
	})
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[0], // c146
		To:         graph.Nodes()[3], // s9600396
		Kind:       routing.EdgeKindSynthetic,
		Duration:   300,
		Transport:  "train",
		IsTransfer: true,
	})

	// Replace the router with our test graph
	h.Router = graph

	// Create request with JSON body
	req := httptest.NewRequest("POST", "/v1/routes/search", strings.NewReader(`{"from_city_id": "c146", "to_city_id": "c213", "date": "2026-08-15"}`))
	req.Header.Set("Content-Type", "application/json")

	rr := httptest.NewRecorder()
	RouteSearch(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp routeSearchResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
		t.Logf("route search response: routes=%+v, count=%d", resp.Routes, resp.Count)
	}
}

func TestHandlerRouteGeoJSON(t *testing.T) {
	h := newMockHandlerContext()

	req := httptest.NewRequest("GET", "/v1/routes/search-123/route-456/geojson", nil)
	rr := httptest.NewRecorder()
	RouteGeoJSON(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp routeGeoJSONResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}
	t.Logf("route geojson response: %+v", resp)
}

func TestRouteGeoJSON(t *testing.T) {
	h := newMockHandlerContext()

	// Add edges to the graph to test GeoJSON generation
	graph := routing.NewGraph()
	graph.AddNode(&routing.Node{ID: "s1", Type: routing.NodeTypeStation, Name: "Station 1", CityCode: "c1"})
	graph.AddNode(&routing.Node{ID: "s2", Type: routing.NodeTypeStation, Name: "Station 2", CityCode: "c1"})
	graph.AddNode(&routing.Node{ID: "s3", Type: routing.NodeTypeStation, Name: "Station 3", CityCode: "c1"})
	// Add a real edge s1 → s2
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[0],
		To:         graph.Nodes()[1],
		Kind:       routing.EdgeKindReal,
		Duration:   3600,
		Transport:  "train",
		TransportType: routing.TransportTypeTrain,
		IsTransfer: false,
		Synthetic:  false,
	})
	// Add a synthetic edge s2 → s3 (city↔airport transfer)
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[1],
		To:         graph.Nodes()[2],
		Kind:       routing.EdgeKindSynthetic,
		Duration:   300,
		Transport:  "train",
		TransportType: routing.TransportTypeTrain,
		IsTransfer: true,
		Synthetic:  true,
	})
	h.Router = graph

	req := httptest.NewRequest("GET", "/v1/routes/search-123/route-456/geojson", nil)
	rr := httptest.NewRecorder()
	RouteGeoJSON(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp routeGeoJSONResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	// Debug: print all feature types and properties
	t.Logf("Total features: %d", len(resp.Features))
	for i, feature := range resp.Features {
		geom, _ := feature["geometry"].(map[string]interface{})
		t.Logf("Feature %d: geom_type=%s", i, geom["type"])
		props, _ := feature["properties"].(map[string]interface{})
		if props != nil {
			t.Logf("Feature %d props: %+v", i, props)
		}
	}

	// Should have features for both real and synthetic edges
	if len(resp.Features) == 0 {
		t.Error("expected at least one feature in GeoJSON response")
	}

	// Check that real and synthetic edges have different stroke styles
	hasReal := false
	hasSynthetic := false
	for _, feature := range resp.Features {
		props, ok := feature["properties"].(map[string]interface{})
		if !ok {
			continue
		}
		dasharray, _ := props["stroke_dasharray"].(string)
		t.Logf("Checking feature: dasharray=%s", dasharray)
		if dasharray == "" {
			hasReal = true
		}
		if dasharray == "5, 5" {
			hasSynthetic = true
		}
	}
	if !hasReal {
		t.Error("expected at least one real edge with solid line (no dasharray)")
	}
	if !hasSynthetic {
		t.Error("expected at least one synthetic edge with dashed line (dasharray=5, 5)")
	}

	t.Logf("GeoJSON response has %d features", len(resp.Features))
}

func TestGeoJSONVisualization(t *testing.T) {
	h := newMockHandlerContext()

	// Add a route with multiple legs and transfer points
	graph := routing.NewGraph()
	graph.AddNode(&routing.Node{ID: "s1", Type: routing.NodeTypeStation, Name: "Moscow", CityCode: "c1"})
	graph.AddNode(&routing.Node{ID: "s2", Type: routing.NodeTypeStation, Name: "Transfer Station", CityCode: "c1"})
	graph.AddNode(&routing.Node{ID: "s3", Type: routing.NodeTypeStation, Name: "Destination", CityCode: "c1"})

	// Add real edge Moscow → Transfer
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[0],
		To:         graph.Nodes()[1],
		Kind:       routing.EdgeKindReal,
		Duration:   1800,
		Transport:  "train",
		TransportType: routing.TransportTypeTrain,
		IsTransfer: false,
		Synthetic:  false,
	})
	// Add transfer edge Transfer → Destination
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[1],
		To:         graph.Nodes()[2],
		Kind:       routing.EdgeKindReal,
		Duration:   1800,
		Transport:  "train",
		TransportType: routing.TransportTypeTrain,
		IsTransfer: true,
		Synthetic:  false,
	})
	h.Router = graph

	req := httptest.NewRequest("GET", "/v1/routes/search-123/route-456/geojson", nil)
	rr := httptest.NewRecorder()
	RouteGeoJSON(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp routeGeoJSONResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	// Should have features for edges
	if len(resp.Features) == 0 {
		t.Error("expected at least one feature in GeoJSON response")
	}

	// Check for transfer point markers (Point geometry with marker_type=transfer)
	hasTransferMarker := false
	for _, feature := range resp.Features {
		geom, ok := feature["geometry"].(map[string]interface{})
		if !ok {
			continue
		}
		geomType, _ := geom["type"].(string)
		if geomType == "Point" {
			props, ok := feature["properties"].(map[string]interface{})
			if ok {
			 markerType, ok := props["marker_type"].(string)
			 if ok && markerType == "transfer" {
				 hasTransferMarker = true
				 // Verify popup-related properties exist
				 _, hasConnTime := props["connection_time"]
				 _, hasTransferType := props["transfer_type"]
				 if !hasConnTime {
					 t.Error("expected connection_time property in transfer marker")
				 }
				 if !hasTransferType {
					 t.Error("expected transfer_type property in transfer marker")
				 }
			 }
			}
		}
	}
	if !hasTransferMarker {
		t.Error("expected at least one transfer point marker with popup info")
	}

	t.Logf("GeoJSON visualization has %d features, including transfer markers", len(resp.Features))
}

func TestHandlerStationStatus(t *testing.T) {
	h := newMockHandlerContext()

	req := httptest.NewRequest("GET", "/v1/stations/s9600213/status", nil)
	rr := httptest.NewRecorder()
	StationStatus(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp stationStatusResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}
	t.Logf("station status response: %+v", resp)
}

func TestAdminAuth(t *testing.T) {
	h := newMockHandlerContext()

	// Test 1: Request without API key should be unauthorized
	req := httptest.NewRequest("POST", "/internal/admin/stations/s9600213/status", strings.NewReader(`{"status": "closed", "source": "manual"}`))
	req.Header.Set("Content-Type", "application/json")
	rr := httptest.NewRecorder()
	AdminStationStatus(h, rr, req)

	if rr.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401 (unauthorized) without API key, got %d", rr.Code)
	}
	t.Logf("Test admin auth (no key): got status %d (expected 401)", rr.Code)

	// Test 2: Request with correct API key should be authorized
	req2 := httptest.NewRequest("POST", "/internal/admin/stations/s9600213/status", strings.NewReader(`{"status": "closed", "source": "manual"}`))
	req2.Header.Set("Content-Type", "application/json")
	req2.Header.Set("X-Admin-Api-Key", "trip-planner-admin-key")
	rr2 := httptest.NewRecorder()
	AdminStationStatus(h, rr2, req2)

	if rr2.Code != http.StatusOK {
		t.Errorf("expected status 200 with valid API key, got %d", rr2.Code)
	}
	t.Logf("Test admin auth (valid key): got status %d (expected 200)", rr2.Code)

	// Test 3: Request with wrong API key should be unauthorized
	req3 := httptest.NewRequest("POST", "/internal/admin/stations/s9600213/status", strings.NewReader(`{"status": "closed", "source": "manual"}`))
	req3.Header.Set("Content-Type", "application/json")
	req3.Header.Set("X-Admin-Api-Key", "wrong-key")
	rr3 := httptest.NewRecorder()
	AdminStationStatus(h, rr3, req3)

	if rr3.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401 (unauthorized) with wrong API key, got %d", rr3.Code)
	}
	t.Logf("Test admin auth (wrong key): got status %d (expected 401)", rr3.Code)
}

func TestAdminStationStatus(t *testing.T) {
	h := newMockHandlerContext()

	// Set up a station in the graph
	graph := routing.NewGraph()
	graph.AddNode(&routing.Node{ID: "s9600213", Type: routing.NodeTypeStation, Name: "Sheremetyevo", CityCode: "c146"})
	h.Router = graph

	// Test 1: Set station status to "closed" with manual source
	req := httptest.NewRequest("POST", "/internal/admin/stations/s9600213/status", strings.NewReader(`{"status": "closed", "source": "manual"}`))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Admin-Api-Key", "trip-planner-admin-key")
	rr := httptest.NewRecorder()
	AdminStationStatus(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp map[string]interface{}
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if resp["status"] != "closed" {
		t.Errorf("expected status 'closed', got '%v'", resp["status"])
	}
	if resp["source"] != "manual" {
		t.Errorf("expected source 'manual', got '%v'", resp["source"])
	}
	if resp["id"] != "s9600213" {
		t.Errorf("expected id 's9600213', got '%v'", resp["id"])
	}
	t.Logf("Test admin station status (closed): %+v", resp)

	// Test 2: Set station status to "active" with manual source
	req2 := httptest.NewRequest("POST", "/internal/admin/stations/s9600213/status", strings.NewReader(`{"status": "active", "source": "manual"}`))
	req2.Header.Set("Content-Type", "application/json")
	req2.Header.Set("X-Admin-Api-Key", "trip-planner-admin-key")
	rr2 := httptest.NewRecorder()
	AdminStationStatus(h, rr2, req2)

	if rr2.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr2.Code)
	}

	var resp2 map[string]interface{}
	if err := json.Unmarshal(rr2.Body.Bytes(), &resp2); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if resp2["status"] != "active" {
		t.Errorf("expected status 'active', got '%v'", resp2["status"])
	}
	t.Logf("Test admin station status (active): %+v", resp2)

	// Test 3: Invalid status value should return 400
	req3 := httptest.NewRequest("POST", "/internal/admin/stations/s9600213/status", strings.NewReader(`{"status": "invalid", "source": "manual"}`))
	req3.Header.Set("Content-Type", "application/json")
	req3.Header.Set("X-Admin-Api-Key", "trip-planner-admin-key")
	rr3 := httptest.NewRecorder()
	AdminStationStatus(h, rr3, req3)

	if rr3.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for invalid status, got %d", rr3.Code)
	}
	t.Logf("Test admin station status (invalid status): got status %d (expected 400)", rr3.Code)

	// Test 4: Invalid source should return 400
	req4 := httptest.NewRequest("POST", "/internal/admin/stations/s9600213/status", strings.NewReader(`{"status": "closed", "source": "geo"}`))
	req4.Header.Set("Content-Type", "application/json")
	req4.Header.Set("X-Admin-Api-Key", "trip-planner-admin-key")
	rr4 := httptest.NewRecorder()
	AdminStationStatus(h, rr4, req4)

	if rr4.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for invalid source, got %d", rr4.Code)
	}
	t.Logf("Test admin station status (invalid source): got status %d (expected 400)", rr4.Code)

	// Test 5: Missing body should return 400
	req5 := httptest.NewRequest("POST", "/internal/admin/stations/s9600213/status", strings.NewReader(""))
	req5.Header.Set("Content-Type", "application/json")
	req5.Header.Set("X-Admin-Api-Key", "trip-planner-admin-key")
	rr5 := httptest.NewRecorder()
	AdminStationStatus(h, rr5, req5)

	if rr5.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for missing body, got %d", rr5.Code)
	}
	t.Logf("Test admin station status (missing body): got status %d (expected 400)", rr5.Code)
}

// TestHandlerRouteSearchIntegration tests the route search handler with a fully built graph,
// verifying the cache-aware flow: handler → graph → route search → response.
func TestHandlerRouteSearchIntegration(t *testing.T) {
	h := newMockHandlerContext()

	// Build a routing graph using the same pattern as TestFindRouteSuccess:
	// stations with real edges and one synthetic transfer edge, plus city hub.
	graph := routing.NewGraph()
	graph.AddNode(&routing.Node{ID: "c1", Type: routing.NodeTypeCity, Name: "City Hub"})
	graph.AddNode(&routing.Node{ID: "s1", Type: routing.NodeTypeStation, Name: "Moscow", CityCode: "c1"})
	graph.AddNode(&routing.Node{ID: "s2", Type: routing.NodeTypeStation, Name: "Tula", CityCode: "c1"})
	graph.AddNode(&routing.Node{ID: "s3", Type: routing.NodeTypeStation, Name: "Vladimir", CityCode: "c1"})

	// Add synthetic edge: city hub <-> station Moscow (transfer)
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[0], // c1 city hub
		To:         graph.Nodes()[1], // s1 Moscow
		Kind:       routing.EdgeKindSynthetic,
		Duration:   300,
		Transport:  "train",
		IsTransfer: true,
	})
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[1], // s1 Moscow
		To:         graph.Nodes()[0], // c1 city hub
		Kind:       routing.EdgeKindSynthetic,
		Duration:   300,
		Transport:  "train",
		IsTransfer: true,
	})

	// Add real edge: direct route Moscow → Tula
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[1], // s1 Moscow
		To:         graph.Nodes()[2], // s2 Tula
		Kind:       routing.EdgeKindReal,
		Duration:   3600,
		Transport:  "train",
		IsTransfer: false,
	})

	// Add synthetic transfer edge: Tula → Vladimir (1 transfer)
	graph.AddEdge(&routing.Edge{
		From:       graph.Nodes()[2], // s2 Tula
		To:         graph.Nodes()[3], // s3 Vladimir
		Kind:       routing.EdgeKindSynthetic,
		Duration:   1800,
		Transport:  "train",
		IsTransfer: true,
	})

	// Replace the router with our test graph
	h.Router = graph

	// Create request: from city c1 (Moscow) to city c1 (same city code)
	req := httptest.NewRequest("POST", "/v1/routes/search", strings.NewReader(`{"from_city_id": "c1", "to_city_id": "c1", "date": "2026-08-15"}`))
	req.Header.Set("Content-Type", "application/json")

	rr := httptest.NewRecorder()
	RouteSearch(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp routeSearchResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}
	t.Logf("route search response: routes=%+v, count=%d", resp.Routes, resp.Count)

	// With this graph, we should find a route with 1 transfer
	if resp.Count == 0 {
		t.Error("expected at least 1 route, got 0")
	}
}

// TestHandlerRouteSearchNoRoute tests route search when origin/destination not in graph.
func TestHandlerRouteSearchNoRoute(t *testing.T) {
	h := newMockHandlerContext()

	// Create graph with no relevant nodes, but add some so the handler can find
	// the city IDs (otherwise handler returns 404 before route search)
	graph := routing.NewGraph()
	graph.AddNode(&routing.Node{ID: "c999", Type: routing.NodeTypeCity, Name: "City 999"})
	graph.AddNode(&routing.Node{ID: "c888", Type: routing.NodeTypeCity, Name: "City 888"})
	h.Router = graph

	req := httptest.NewRequest("POST", "/v1/routes/search", strings.NewReader(`{"from_city_id": "c999", "to_city_id": "c888", "date": "2026-08-15"}`))
	req.Header.Set("Content-Type", "application/json")

	rr := httptest.NewRecorder()
	RouteSearch(h, rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
	}

	var resp routeSearchResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}
	if resp.Count != 0 {
		t.Errorf("expected 0 routes, got %d", resp.Count)
	}
}

// TestNeighboringStations tests the StationNeighbor type from the airports package.
func TestNeighboringStations(t *testing.T) {
	// Test creating neighbors with different sources
	n := airports.NewStationNeighbors("c1")
	n.Add("s1", "Station One", "geo")
	n.Add("s2", "Station Two", "manual")

	if n.Len() != 2 {
		t.Errorf("expected 2 neighbors, got %d", n.Len())
	}

	// Test marking as excluded
	n.MarkExcluded("s1")
	isExcluded, found := n.IsExcluded("s1")
	if !found {
		t.Error("expected s1 to be found in neighbors")
	}
	if !isExcluded {
		t.Error("expected s1 to be excluded")
	}

	// Test getting non-excluded neighbors
	nonExcluded := n.GetNonExcluded()
	if len(nonExcluded) != 1 {
		t.Errorf("expected 1 non-excluded neighbor, got %d", len(nonExcluded))
	}
	if nonExcluded[0].Name != "Station Two" {
		t.Errorf("expected 'Station Two' as non-excluded, got %s", nonExcluded[0].Name)
	}

	// Test getting neighbor by ID
	neighbor, found := n.Get("s2")
	if !found {
		t.Error("expected s2 to be found")
	}
	if neighbor.Name != "Station Two" {
		t.Errorf("expected 'Station Two', got %s", neighbor.Name)
	}

	// Test sorting
	// Note: ASCII order has 'O' < 'T' < 'Z', so "Station One" < "Station Two" < "Station Zero"
	n.Add("s0", "Station Zero", "geo")
	n.Sort()
	if n.Neighbors[0].Name != "Station One" {
		t.Errorf("expected 'Station One' first after sort (alphabetical), got %s", n.Neighbors[0].Name)
	}
	if n.Neighbors[1].Name != "Station Two" {
		t.Errorf("expected 'Station Two' second after sort, got %s", n.Neighbors[1].Name)
	}
	if n.Neighbors[2].Name != "Station Zero" {
		t.Errorf("expected 'Station Zero' third after sort, got %s", n.Neighbors[2].Name)
	}
}

// TestStationNeighbors tests the storage.StationNeighborsTable type.
func TestStationNeighbors(t *testing.T) {
	// Test adding neighbors for a city
	table := storage.NewStationNeighborsTable()
	table.Add("c1", "s1", "Moscow Station", "manual")
	table.Add("c1", "s2", "Tula Station", "manual")
	table.Add("c1", "s3", "Kursk Station", "geo")

	// Get all neighbors for city c1
	neighbors := table.GetByCity("c1")
	if len(neighbors) != 3 {
		t.Errorf("expected 3 neighbors for city c1, got %d", len(neighbors))
	}

	// Get non-excluded neighbors
	nonExcluded := table.GetNonExcluded("c1")
	if len(nonExcluded) != 3 {
		t.Errorf("expected 3 non-excluded neighbors for city c1, got %d", len(nonExcluded))
	}

	// Mark one as excluded
	table.MarkExcluded("c1", "s2")
	nonExcludedAfter := table.GetNonExcluded("c1")
	if len(nonExcludedAfter) != 2 {
		t.Errorf("expected 2 non-excluded neighbors after marking s2 excluded, got %d", len(nonExcludedAfter))
	}

	// Verify the excluded one is not in the list
	foundS2 := false
	for _, n := range nonExcludedAfter {
		if n.StationID == "s2" {
			foundS2 = true
		}
	}
	if foundS2 {
		t.Error("expected s2 to be excluded from non-excluded list")
	}
}

// TestUserPreferences tests the user preferences functionality via API handlers.
func TestUserPreferences(t *testing.T) {
	h := newMockHandlerContext()

	t.Run("get_saved_cities_empty", func(t *testing.T) {
		// Flush preference-related Redis keys for test isolation
		flushRedisForTest(t, h.Redis)

		req := httptest.NewRequest("GET", "/v1/preferences/saved-cities?user_id=testuser1", nil)
		rr := httptest.NewRecorder()
		GetSavedCities(h, rr, req)

		if rr.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", rr.Code)
		}

		var resp preferenceResponse
		if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
			t.Fatalf("failed to unmarshal response: %v", err)
		}

		if resp.Message != "saved cities retrieved" {
			t.Errorf("expected message 'saved cities retrieved', got '%s'", resp.Message)
		}

		if len(resp.Data.([]interface{})) != 0 {
			t.Errorf("expected empty list of saved cities, got %d", len(resp.Data.([]interface{})))
		}
	})

	t.Run("add_and_get_saved_city", func(t *testing.T) {
		// Flush preference-related Redis keys for test isolation
		flushRedisForTest(t, h.Redis)

		// Add a saved city
		addReq := httptest.NewRequest("POST", "/v1/preferences/saved-cities?user_id=testuser2", strings.NewReader(`{"city_code":"c1","name":"Moscow"}`,))
		addReq.Header.Set("Content-Type", "application/json")
		addRR := httptest.NewRecorder()
		AddSavedCity(h, addRR, addReq)

		if addRR.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", addRR.Code)
		}

		var addResp preferenceResponse
		if err := json.Unmarshal(addRR.Body.Bytes(), &addResp); err != nil {
			t.Fatalf("failed to unmarshal add response: %v", err)
		}
		if addResp.Message != "saved city added" {
			t.Errorf("expected message 'saved city added', got '%s'", addResp.Message)
		}

		// Get saved cities
		getReq := httptest.NewRequest("GET", "/v1/preferences/saved-cities?user_id=testuser2", nil)
		getRR := httptest.NewRecorder()
		GetSavedCities(h, getRR, getReq)

		if getRR.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", getRR.Code)
		}

		var getResp preferenceResponse
		if err := json.Unmarshal(getRR.Body.Bytes(), &getResp); err != nil {
			t.Fatalf("failed to unmarshal response: %v", err)
		}

		cities := getResp.Data.([]interface{})
		if len(cities) != 1 {
			t.Errorf("expected 1 saved city, got %d", len(cities))
		} else {
			city := cities[0].(map[string]interface{})
			if city["city_code"] != "c1" {
				t.Errorf("expected city_code 'c1', got '%v'", city["city_code"])
			}
			if city["name"] != "Moscow" {
				t.Errorf("expected name 'Moscow', got '%v'", city["name"])
			}
		}
	})

	t.Run("remove_saved_city", func(t *testing.T) {
		// Flush preference-related Redis keys for test isolation
		flushRedisForTest(t, h.Redis)

		// Remove the previously added city
		removeReq := httptest.NewRequest("DELETE", "/v1/preferences/saved-cities/c1?user_id=testuser3", nil)
		removeRR := httptest.NewRecorder()
		RemoveSavedCity(h, removeRR, removeReq)

		if removeRR.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", removeRR.Code)
		}

		var removeResp preferenceResponse
		if err := json.Unmarshal(removeRR.Body.Bytes(), &removeResp); err != nil {
			t.Fatalf("failed to unmarshal response: %v", err)
		}
		if removeResp.Message != "saved city removed" {
			t.Errorf("expected message 'saved city removed', got '%s'", removeResp.Message)
		}

		// Verify city is gone
		getReq := httptest.NewRequest("GET", "/v1/preferences/saved-cities?user_id=testuser3", nil)
		getRR := httptest.NewRecorder()
		GetSavedCities(h, getRR, getReq)

		if getRR.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", getRR.Code)
		}

		var getResp preferenceResponse
		if err := json.Unmarshal(getRR.Body.Bytes(), &getResp); err != nil {
			t.Fatalf("failed to unmarshal response: %v", err)
		}

		cities := getResp.Data.([]interface{})
		if len(cities) != 0 {
			t.Errorf("expected 0 saved cities after removal, got %d", len(cities))
		}
	})

	t.Run("get_search_history_empty", func(t *testing.T) {
		// Flush preference-related Redis keys for test isolation
		flushRedisForTest(t, h.Redis)

		req := httptest.NewRequest("GET", "/v1/preferences/search-history?user_id=testuser4", nil)
		rr := httptest.NewRecorder()
		GetSearchHistory(h, rr, req)

		if rr.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", rr.Code)
		}

		var resp preferenceResponse
		if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
			t.Fatalf("failed to unmarshal response: %v", err)
		}

		if resp.Message != "search history retrieved" {
			t.Errorf("expected message 'search history retrieved', got '%s'", resp.Message)
		}

		if len(resp.Data.([]interface{})) != 0 {
			t.Errorf("expected empty list of search history, got %d", len(resp.Data.([]interface{})))
		}
	})

	t.Run("add_and_get_search_history", func(t *testing.T) {
		// Flush preference-related Redis keys for test isolation
		flushRedisForTest(t, h.Redis)

		// Add a search history entry
		addReq := httptest.NewRequest("POST", "/v1/preferences/search-history?user_id=testuser5", strings.NewReader(`{"from_city":"c1","to_city":"c2","date":"2026-08-15"}`))
		addReq.Header.Set("Content-Type", "application/json")
		addRR := httptest.NewRecorder()
		AddSearchHistory(h, addRR, addReq)

		if addRR.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", addRR.Code)
		}

		var addResp preferenceResponse
		if err := json.Unmarshal(addRR.Body.Bytes(), &addResp); err != nil {
			t.Fatalf("failed to unmarshal add response: %v", err)
		}
		if addResp.Message != "search history added" {
			t.Errorf("expected message 'search history added', got '%s'", addResp.Message)
		}

		// Get search history
		getReq := httptest.NewRequest("GET", "/v1/preferences/search-history?user_id=testuser5", nil)
		getRR := httptest.NewRecorder()
		GetSearchHistory(h, getRR, getReq)

		if getRR.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", getRR.Code)
		}

		var getResp preferenceResponse
		if err := json.Unmarshal(getRR.Body.Bytes(), &getResp); err != nil {
			t.Fatalf("failed to unmarshal response: %v", err)
		}

		history := getResp.Data.([]interface{})
		if len(history) != 1 {
			t.Errorf("expected 1 search history entry, got %d", len(history))
		} else {
			entry := history[0].(map[string]interface{})
			if entry["from_city"] != "c1" {
				t.Errorf("expected from_city 'c1', got '%v'", entry["from_city"])
			}
			if entry["to_city"] != "c2" {
				t.Errorf("expected to_city 'c2', got '%v'", entry["to_city"])
			}
			if entry["date"] != "2026-08-15" {
				t.Errorf("expected date '2026-08-15', got '%v'", entry["date"])
			}
		}
	})

	t.Run("remove_old_search_history", func(t *testing.T) {
		// Flush preference-related Redis keys for test isolation
		flushRedisForTest(t, h.Redis)

		// Add a search history entry via Preferences
		err := h.Preferences.AddSearchHistory(context.Background(), "testuser7", "c1", "c2", "2026-08-10")
		if err != nil {
			t.Fatalf("failed to add search history: %v", err)
		}

		// Add another entry with old timestamp (CreatedAt set to 200 seconds ago)
		// Since we can't easily get time.Now() in tests without the time import,
		// we test by adding an entry and then removing old entries.
		// The RemoveOldSearchHistory function should filter by age.

		// Remove old history (maxAge=100 seconds)
		err = h.Preferences.RemoveOldSearchHistory(context.Background(), "testuser7", 100)
		if err != nil {
			t.Fatalf("failed to remove old search history: %v", err)
		}

		// Verify by getting history directly - entries should still exist
		// (since we added one without specifying a past timestamp, and
		// RemoveOldSearchHistory with maxAge=100 would only remove very old entries)
		history, err := h.Preferences.GetSearchHistory(context.Background(), "testuser7")
		if err != nil {
			t.Fatalf("failed to get search history: %v", err)
		}
		// At minimum, the entry we just added should be in history
		if len(history) == 0 {
			t.Error("expected at least 1 search history entry")
		}
	})
}