import { describe, it, expect } from "vitest";
import {
  routesToPolylines,
  intermediateCityIds,
} from "./routesToPolylines.js";
import { transformDictionaries } from "@/shared/dictionaries/index.js";
import type {
  IFlightRoute,
  IFlightsMapFilterState,
} from "./types.js";
import type { IDictionaries, IRawDictionaries } from "@/shared/dictionaries/index.js";

// Empty-dictionary fixture for tests that don't care about normalization.
const EMPTY_DICT: IDictionaries = transformDictionaries(
  { regions: [], countries: [], cities: [], airports: [] },
  "ru",
);

function filter(
  overrides: Partial<IFlightsMapFilterState> = {},
): IFlightsMapFilterState {
  return {
    connections: false,
    domestic: false,
    international: false,
    ...overrides,
  };
}

describe("routesToPolylines — empty input", () => {
  it("returns [] for no routes", () => {
    expect(routesToPolylines([], filter({ departure: "A" }), EMPTY_DICT)).toEqual([]);
  });
});

describe("routesToPolylines — spider mode (departure only)", () => {
  it("produces one polyline per unique last-hop destination", () => {
    const routes: IFlightRoute[] = [
      { route: ["A", "B"], isDirect: true },
      { route: ["A", "C"], isDirect: true },
      { route: ["A", "B"], isDirect: true },
    ];

    const pls = routesToPolylines(routes, filter({ departure: "A" }), EMPTY_DICT);

    expect(pls).toHaveLength(2);
    expect(pls.every((p) => p.cityIds[0] === "A")).toBe(true);
    expect(pls.every((p) => p.cityIds.length === 2)).toBe(true);
    expect(pls.every((p) => p.style === "direct")).toBe(true);
    const dests = pls.map((p) => p.cityIds[1]).sort();
    expect(dests).toEqual(["B", "C"]);
  });

  it("drops routes whose last hop equals departure", () => {
    const routes: IFlightRoute[] = [
      { route: ["A", "A"], isDirect: true },
      { route: ["A", "B"], isDirect: true },
    ];
    const pls = routesToPolylines(routes, filter({ departure: "A" }), EMPTY_DICT);
    expect(pls).toHaveLength(1);
    expect(pls[0]!.cityIds).toEqual(["A", "B"]);
  });

  it("skips single-city routes (len < 2)", () => {
    const routes: IFlightRoute[] = [{ route: ["A"], isDirect: true }];
    const pls = routesToPolylines(routes, filter({ departure: "A" }), EMPTY_DICT);
    expect(pls).toEqual([]);
  });
});

describe("routesToPolylines — route mode (departure + arrival)", () => {
  it("direct route gets style=\"direct\"", () => {
    const routes: IFlightRoute[] = [{ route: ["A", "B"], isDirect: true }];
    const pls = routesToPolylines(routes, filter({ departure: "A", arrival: "B" }), EMPTY_DICT);
    expect(pls).toHaveLength(1);
    expect(pls[0]!.style).toBe("direct");
    expect(pls[0]!.cityIds).toEqual(["A", "B"]);
  });

  it("non-direct route gets style=\"connecting\" (hop count irrelevant)", () => {
    const routes: IFlightRoute[] = [
      { route: ["A", "X", "B"], isDirect: false },
      { route: ["A", "B"], isDirect: false },
    ];
    const pls = routesToPolylines(routes, filter({ departure: "A", arrival: "B" }), EMPTY_DICT);
    expect(pls.every((p) => p.style === "connecting")).toBe(true);
  });

  it("assigns unique IDs across multiple routes", () => {
    const routes: IFlightRoute[] = [
      { route: ["A", "B"], isDirect: true },
      { route: ["A", "X", "B"], isDirect: false },
      { route: ["A", "B"], isDirect: false },
    ];
    const pls = routesToPolylines(routes, filter({ departure: "A", arrival: "B" }), EMPTY_DICT);
    const ids = new Set(pls.map((p) => p.id));
    expect(ids.size).toBe(pls.length);
  });
});

describe("intermediateCityIds", () => {
  it("returns [] for no routes", () => {
    expect(intermediateCityIds([], EMPTY_DICT)).toEqual([]);
  });

  it("ignores 2-city routes", () => {
    expect(intermediateCityIds([{ route: ["A", "B"], isDirect: true }], EMPTY_DICT)).toEqual([]);
  });

  it("extracts the single inner city from a 3-city route", () => {
    expect(
      intermediateCityIds([{ route: ["A", "X", "B"], isDirect: false }], EMPTY_DICT),
    ).toEqual(["X"]);
  });

  it("extracts both inner cities from a 4-city route", () => {
    expect(
      intermediateCityIds([{ route: ["A", "X", "Y", "B"], isDirect: false }], EMPTY_DICT).sort(),
    ).toEqual(["X", "Y"]);
  });

  it("dedups intermediates across multiple routes", () => {
    const ids = intermediateCityIds([
      { route: ["A", "X", "B"], isDirect: false },
      { route: ["C", "X", "D"], isDirect: false },
    ], EMPTY_DICT);
    expect(ids).toEqual(["X"]);
  });
});

// ---------------------------------------------------------------------------
// Airport-code normalization (C.4)
// ---------------------------------------------------------------------------

const rawWithAirports: IRawDictionaries = {
  regions: [],
  countries: [{ code: "RU", title: { ru: "Россия" }, world_region_id: 500374 }],
  cities: [
    { code: "MOW", title: { ru: "Москва" }, country_code: "RU", has_afl_flights: true, location: { lat: 55, lon: 37 } },
    { code: "LED", title: { ru: "Санкт-Петербург" }, country_code: "RU", has_afl_flights: true, location: { lat: 60, lon: 30 } },
  ],
  airports: [
    { code: "SVO", city_code: "MOW", title: { ru: "Шереметьево" }, has_afl_flights: true, location: { lat: 55, lon: 37 } },
    { code: "LED", city_code: "LED", title: { ru: "Пулково" }, has_afl_flights: true, location: { lat: 60, lon: 30 } },
  ],
};
const DICT_WITH_AIRPORTS: IDictionaries = transformDictionaries(rawWithAirports, "ru");

describe("routesToPolylines — airport-code normalization", () => {
  it("normalizes route-mode cityIds from airport codes to city codes", () => {
    const routes: IFlightRoute[] = [{ route: ["SVO", "LED"], isDirect: true }];
    const pls = routesToPolylines(
      routes,
      filter({ departure: "MOW", arrival: "LED" }),
      DICT_WITH_AIRPORTS,
    );
    expect(pls).toHaveLength(1);
    expect(pls[0]!.cityIds).toEqual(["MOW", "LED"]);
  });

  it("normalizes spider-mode cityIds", () => {
    const routes: IFlightRoute[] = [{ route: ["SVO", "LED"], isDirect: true }];
    const pls = routesToPolylines(
      routes,
      filter({ departure: "MOW" }),
      DICT_WITH_AIRPORTS,
    );
    expect(pls).toHaveLength(1);
    expect(pls[0]!.cityIds).toEqual(["MOW", "LED"]);
  });

  it("intermediateCityIds returns city codes when airport codes appear inside a multi-hop route", () => {
    const routes: IFlightRoute[] = [
      { route: ["LED", "SVO", "MOW"], isDirect: false },
    ];
    expect(intermediateCityIds(routes, DICT_WITH_AIRPORTS)).toEqual(["MOW"]);
  });
});

// ---------------------------------------------------------------------------
// TZ §4.1.24.3 R21/R27/R28/R29: rendering mode selection assertions
// ---------------------------------------------------------------------------

describe("§4.1.24.3 R21: dot-network mode (no polylines) when routes is empty", () => {
  it("4.1.24-R21: returns [] when routes array is empty (dots-only fallback)", () => {
    // No departure → no polylines → MapCanvas renders city dots only
    expect(routesToPolylines([], filter(), EMPTY_DICT)).toEqual([]);
  });

  it("4.1.24-R29: caller must pass [] routes to trigger dot fallback (no departure + no arrival)", () => {
    // With departure set but 0 routes, caller returns empty polylines → dots
    expect(routesToPolylines([], filter({ departure: "A" }), EMPTY_DICT)).toEqual([]);
  });
});

describe("§4.1.24.3 R25: arc style matches route type (direct=solid, connecting=dashed)", () => {
  it("4.1.24-R25: isDirect=true yields style=\"direct\" (solid arc)", () => {
    const pls = routesToPolylines(
      [{ route: ["A", "B"], isDirect: true }],
      filter({ departure: "A", arrival: "B" }),
      EMPTY_DICT,
    );
    expect(pls[0]!.style).toBe("direct");
  });

  it("4.1.24-R25: isDirect=false yields style=\"connecting\" (dashed arc)", () => {
    const pls = routesToPolylines(
      [{ route: ["A", "B"], isDirect: false }],
      filter({ departure: "A", arrival: "B" }),
      EMPTY_DICT,
    );
    expect(pls[0]!.style).toBe("connecting");
  });
});

describe("§4.1.24.3 R27/R28: direct-route precedence logic (upstream filter responsibility)", () => {
  it("4.1.24-R27: when direct routes exist in the input, direct style is used (precedence is caller's)", () => {
    // The TZ precedence (direct > connecting) is enforced by the caller in
    // FlightsMapStartPage (auto-fallback with connections=0 first, then 1).
    // routesToPolylines just maps isDirect → style faithfully.
    const mixed = [
      { route: ["A", "B"], isDirect: true },
      { route: ["A", "X", "B"], isDirect: false },
    ];
    const pls = routesToPolylines(mixed, filter({ departure: "A", arrival: "B" }), EMPTY_DICT);
    const directCount = pls.filter((p) => p.style === "direct").length;
    const connectingCount = pls.filter((p) => p.style === "connecting").length;
    expect(directCount).toBe(1);
    expect(connectingCount).toBe(1);
  });

  it("4.1.24-R28: when only connecting routes returned, all arcs are dashed", () => {
    const connectingOnly = [
      { route: ["A", "X", "B"], isDirect: false },
      { route: ["A", "Y", "B"], isDirect: false },
    ];
    const pls = routesToPolylines(connectingOnly, filter({ departure: "A", arrival: "B" }), EMPTY_DICT);
    expect(pls.every((p) => p.style === "connecting")).toBe(true);
  });
});
