/**
 * Tests for ScheduleStartPage component.
 *
 * Verifies query param building from popular requests and
 * form state initialization from URL search params.
 *
 * @vitest-environment jsdom
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { ScheduleStartPage } from "./ScheduleStartPage.js";
import { sessionStore } from "@/shared/storage.js";
import type { PopularRequest } from "@/features/popular-requests/types.js";
import {
  resetCrossSectionStore,
  setBoardFilter,
  setScheduleFilter,
  type BoardFilterSnapshot,
} from "@/shared/state/crossSectionNavigation.js";

const mockNavigate = vi.fn();

vi.mock("@modern-js/runtime/router", () => ({
  useNavigate: () => mockNavigate,
  useParams: () => ({ lang: "ru-ru" }),
  Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => (
    <a href={to} {...props}>{children}</a>
  ),
}));

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({
    t: (key: string) => key,
  }),
}));

vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () => ({
  PopularRequestsPanel: ({ onRequestClick }: { onRequestClick?: (r: PopularRequest) => void }) => (
    <div data-testid="popular-requests">
      <button
        data-testid="popular-click-route"
        onClick={() => onRequestClick?.({ mode: "Route", departure: "SVO", arrival: "LED", type: "Schedule" })}
      >
        Route
      </button>
      <button
        data-testid="popular-click-roundtrip"
        onClick={() => onRequestClick?.({ mode: "RouteWithBack", departure: "SVO", arrival: "LED", type: "Schedule" })}
      >
        RouteWithBack
      </button>
      <button
        data-testid="popular-click-onlineboard"
        onClick={() => onRequestClick?.({ mode: "Departure", departure: "LED", type: "Onlineboard" })}
      >
        Onlineboard
      </button>
    </div>
  ),
}));

vi.mock("@/features/online-board/components/OnlineBoardStartPage.js", () => ({
  buildOnlineBoardPrefillState: (request: PopularRequest) => {
    if (request.mode === "Departure") {
      return { tab: "route", departure: request.departure };
    }
    return {};
  },
  ONLINE_BOARD_PREFILL_SLOT: "online-board",
  SCHEDULE_PREFILL_SLOT: "schedule",
}));

vi.mock("@/shared/hooks/useCitySearch.js", () => ({
  useCitySearch: () => ({ suggestions: [], search: vi.fn() }),
}));

vi.mock("@/ui/city-autocomplete/index.js", () => ({
  // Controlled mock — reflects `value` prop changes so tests can assert
  // post-update form state, not just mount-time prefill.
  CityAutocomplete: (props: Record<string, unknown>) => (
    <input
      data-testid={`${(props["testIdPrefix"] as string) ?? "city-autocomplete"}-input`}
      value={(props["value"] as string) ?? ""}
      readOnly
    />
  ),
  SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
    <button type="button" data-testid={props.testId} onClick={props.onClick} />
  ),
}));

vi.mock("@/ui/layout/SearchHistory.js", () => ({
  SearchHistory: () => <div data-testid="search-history" />,
}));

// Dictionaries aren't needed for the popular-request behavior the test
// exercises — return nulls so toCityCode() falls back to the raw code
// (test assertions expect the unresolved SVO/LED codes).
vi.mock("@/shared/dictionaries/index.js", () => ({
  useDictionaries: () => ({ dictionaries: null, loading: false, error: null }),
  getCityCodeByAirportCode: () => undefined,
}));

let geoMockEnabled = false;

vi.mock("@/shared/hooks/useGeoCityDefault.js", () => ({
  useGeoCityDefault: (opts: { shouldApply: () => boolean; onCity: (code: string) => void }) => {
    // If the test enabled geo, fire it synchronously so we don't need async.
    if (geoMockEnabled && opts.shouldApply()) {
      opts.onCity("MOW");
    }
  },
}));

describe("ScheduleStartPage", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    sessionStore.clear();
    resetCrossSectionStore();
    geoMockEnabled = false;
    // Freeze clock for deterministic week bounds
    vi.useFakeTimers();
    vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0)); // Fri 2026-05-15
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("renders the start page", () => {
    render(<ScheduleStartPage />);
    expect(screen.getByTestId("schedule-start")).toBeTruthy();
  });

  // ---------------------------------------------------------------------------
  // TZ §4.1.7: start-page info sections per TZ Table 9
  // ---------------------------------------------------------------------------

  describe("4.1.7: start-page info sections per TZ Table 9", () => {
    it("4.1.7-R-info-heading-1: renders 'Как пользоваться расписанием?' heading", () => {
      render(<ScheduleStartPage />);
      expect(screen.getByText("SCHEDULE.SCHEDULE-START")).toBeTruthy();
    });

    it("4.1.7-R-info-block-1a: renders 'Маршрут' block with description", () => {
      render(<ScheduleStartPage />);
      expect(screen.getByText("SCHEDULE.SCHEDULE-START-TITLE1")).toBeTruthy();
      expect(screen.getByText("SCHEDULE.SCHEDULE-START-TITLE1-DESCRIPTION")).toBeTruthy();
    });

    it("4.1.7-R-info-block-1b: renders 'Дата вылета' block with description", () => {
      render(<ScheduleStartPage />);
      expect(screen.getByText("SCHEDULE.SCHEDULE-START-TITLE2")).toBeTruthy();
      expect(screen.getByText("SCHEDULE.SCHEDULE-START-TITLE2-DESCRIPTION")).toBeTruthy();
    });

    it("4.1.7-R-info-block-1c: renders 'Время вылета' block with description", () => {
      render(<ScheduleStartPage />);
      expect(screen.getByText("SCHEDULE.SCHEDULE-START-TITLE3")).toBeTruthy();
      expect(screen.getByText("SCHEDULE.SCHEDULE-START-TITLE3-DESCRIPTION")).toBeTruthy();
    });

    it("4.1.7-R-info-block-1d: renders 'Обратные рейсы' block with description", () => {
      render(<ScheduleStartPage />);
      expect(screen.getByText("SCHEDULE.SCHEDULE-START-TITLE4")).toBeTruthy();
      expect(screen.getByText("SCHEDULE.SCHEDULE-START-TITLE4-DESCRIPTION")).toBeTruthy();
    });

    // TZ Table 9 prescribes a second 'Возможности расписания' info section
    // (Title5 'Купить билет' + Title6 'Расписание'), but the Angular
    // reference baseline has that block commented out — the infos are
    // redundant with the OB start-page's capability tiles. Following
    // Angular: assert the block is *not* rendered. If business later
    // requires it, flip these to positive assertions.
    it("4.1.7-R-info-block-2a: 'Купить билет' capabilities block (TITLE5) hidden (Angular parity)", () => {
      render(<ScheduleStartPage />);
      expect(screen.queryByText("SCHEDULE.SCHEDULE-START-TITLE5")).toBeNull();
      expect(screen.queryByText("SCHEDULE.SCHEDULE-START-TITLE5-DESCRIPTION")).toBeNull();
    });

    it("4.1.7-R-info-block-2b: 'Расписание' capabilities block (TITLE6) hidden (Angular parity)", () => {
      render(<ScheduleStartPage />);
      expect(screen.queryByText("SCHEDULE.SCHEDULE-START-TITLE6")).toBeNull();
      expect(screen.queryByText("SCHEDULE.SCHEDULE-START-TITLE6-DESCRIPTION")).toBeNull();
    });

    it("4.1.7-R-popular: renders PopularRequestsPanel for request handling", () => {
      render(<ScheduleStartPage />);
      expect(screen.getByTestId("popular-requests")).toBeTruthy();
    });
  });

  it("4.1.5-S1: one-way Route click populates form with current ISO week dates (from clamped to today-1) + no return", () => {
    // 2026-05-15 (Fri) → raw Mon 2026-05-11, raw Sun 2026-05-17
    // `from` is clamped to today−1 = 2026-05-14 so the route guard does
    // not redirect the search back to the start page.
    render(<ScheduleStartPage />);
    fireEvent.click(screen.getByTestId("popular-click-route"));
    expect(mockNavigate).not.toHaveBeenCalled();
    expect((screen.getByTestId("schedule-departure-input") as HTMLInputElement).value).toBe("SVO");
    expect((screen.getByTestId("schedule-arrival-input") as HTMLInputElement).value).toBe("LED");
    expect((screen.getByTestId("round-trip-toggle") as HTMLInputElement).checked).toBe(false);
    expect(screen.queryByTestId("return-date-range-input")).toBeNull();

    fireEvent.submit(screen.getByTestId("schedule-search-form"));
    expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/schedule/route/SVO-LED-20260514-20260517");
  });

  it("4.1.5-S2: round-trip RouteWithBack click populates form with current + next week dates (outbound from clamped)", () => {
    // current week raw: 20260511-20260517 (clamped from: 20260514-20260517)
    // next week: 20260518-20260524 (unclamped — future)
    render(<ScheduleStartPage />);
    fireEvent.click(screen.getByTestId("popular-click-roundtrip"));
    expect(mockNavigate).not.toHaveBeenCalled();
    expect((screen.getByTestId("round-trip-toggle") as HTMLInputElement).checked).toBe(true);

    fireEvent.submit(screen.getByTestId("schedule-search-form"));
    expect(mockNavigate).toHaveBeenCalledWith(
      "/ru-ru/schedule/route/SVO-LED-20260514-20260517/LED-SVO-20260518-20260524",
    );
  });

  it("4.1.5-S3: prefill dates hydrate into form calendar state (no search on mount)", () => {
    sessionStore.setRaw(
      "afl-prefill:schedule",
      JSON.stringify({
        departure: "SVO", arrival: "LED", withReturn: false,
        dateFrom: "20260511", dateTo: "20260517",
      }),
    );
    render(<ScheduleStartPage />);
    // No navigation should have been triggered
    expect(mockNavigate).not.toHaveBeenCalled();
    // Round-trip checkbox must remain unchecked (withReturn=false)
    const roundTripCheckbox = screen.getByTestId("round-trip-toggle");
    expect((roundTripCheckbox as HTMLInputElement).checked).toBe(false);
  });

  it("4.1.5-S4: round-trip prefill sets isRoundTrip = true on mount", () => {
    sessionStore.setRaw(
      "afl-prefill:schedule",
      JSON.stringify({
        departure: "SVO", arrival: "LED", withReturn: true,
        dateFrom: "20260511", dateTo: "20260517",
        returnDateFrom: "20260518", returnDateTo: "20260524",
      }),
    );
    render(<ScheduleStartPage />);
    expect(mockNavigate).not.toHaveBeenCalled();
    const roundTripCheckbox = screen.getByTestId("round-trip-toggle");
    expect((roundTripCheckbox as HTMLInputElement).checked).toBe(true);
  });

  it("Onlineboard-type Departure popular click stays on Schedule and sets departure only", () => {
    // Deviation from Angular: Angular always navigates Arrival/Departure
    // popular clicks to /onlineboard. We instead populate the relevant
    // Schedule field in-place so users planning a route don't lose context.
    render(<ScheduleStartPage />);
    fireEvent.click(screen.getByTestId("popular-click-onlineboard"));
    expect(mockNavigate).not.toHaveBeenCalled();
    expect((screen.getByTestId("schedule-departure-input") as HTMLInputElement).value).toBe("LED");
    expect((screen.getByTestId("schedule-arrival-input") as HTMLInputElement).value).toBe("");
    // Onlineboard-type clicks must not write to either prefill slot.
    expect(sessionStore.getRaw("afl-prefill:online-board")).toBeNull();
  });

  it("initializes form from sessionStorage prefill (legacy shape — withReturn only)", () => {
    sessionStore.setRaw(
      "afl-prefill:schedule",
      JSON.stringify({ departure: "SVO", arrival: "LED", withReturn: true }),
    );
    render(<ScheduleStartPage />);
    const roundTripCheckbox = screen.getByTestId("round-trip-toggle");
    expect((roundTripCheckbox as HTMLInputElement).checked).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// TZ §4.1.9 Table 14: current-week label on Schedule date-range picker
// ---------------------------------------------------------------------------

describe("4.1.9-R: Current-Week label substitution", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    sessionStore.clear();
    resetCrossSectionStore();
    // Clock frozen to Fri 2026-05-15 → Mon 2026-05-11 … Sun 2026-05-17
    vi.useFakeTimers();
    vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("4.1.9-R: start page populates date range with current week on Route click", () => {
    render(<ScheduleStartPage />);
    fireEvent.click(screen.getByTestId("popular-click-route"));
    // Current week Sun for 2026-05-15 is 2026-05-17; `from` is clamped to
    // today−1 = 2026-05-14 so the range is inside Schedule's −1/+330 window.
    fireEvent.submit(screen.getByTestId("schedule-search-form"));
    expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/schedule/route/SVO-LED-20260514-20260517");
  });
});

// ---------------------------------------------------------------------------
// TZ §4.1.8: cross-section hydration tests (Board → Schedule)
// ---------------------------------------------------------------------------

describe("4.1.8: Schedule hydrates from cross-section store on mount", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    sessionStore.clear();
    resetCrossSectionStore();
  });

  it("4.1.8: hydrates departure + arrival from Board filter (projected) when no own state", () => {
    const boardSnap: BoardFilterSnapshot = {
      mode: "route", departure: "MOW", arrival: "LED",
      date: "20260515", timeFrom: "0600", timeTo: "2200",
      searchExecuted: true,
    };
    setBoardFilter(boardSnap);
    render(<ScheduleStartPage />);
    const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
    expect(depInput.defaultValue).toBe("MOW");
    const arrInput = screen.getByTestId("schedule-arrival-input") as HTMLInputElement;
    expect(arrInput.defaultValue).toBe("LED");
  });

  it("4.1.8: uses own schedule snapshot (not board) when schedule state is set", () => {
    setScheduleFilter({
      mode: "route", departure: "KZN", arrival: "AER",
      dateFrom: "20260511", dateTo: "20260517",
      timeFrom: "0000", timeTo: "2400",
      onlyDirect: false, showReturn: false, searchExecuted: false,
    });
    // Also set board — it should be ignored since schedule snapshot is present
    setBoardFilter({
      mode: "route", departure: "SVO", arrival: "VVO",
      date: "20260515", timeFrom: "0000", timeTo: "2400",
      searchExecuted: true,
    });
    render(<ScheduleStartPage />);
    const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
    expect(depInput.defaultValue).toBe("KZN");
  });

  it("4.1.8: renders empty filter when both schedule and board store are empty", () => {
    render(<ScheduleStartPage />);
    const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
    expect(depInput.defaultValue).toBe("");
  });
});

// ---------------------------------------------------------------------------
// TZ §4.1.1-R8/R10: first-entry geolocation auto-fill + time default
// ---------------------------------------------------------------------------

describe("4.1.1-R8/R10: Schedule first-entry geolocation + time default", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    sessionStore.clear();
    resetCrossSectionStore();
    geoMockEnabled = false;
  });

  it("4.1.1-R8: populates departure with geolocated city on first entry", () => {
    geoMockEnabled = true; // mock will call onCity("MOW") synchronously on shouldApply()
    render(<ScheduleStartPage />);
    const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
    expect(depInput.defaultValue).toBe("MOW");
  });

  it("4.1.1-R8: does not populate when geolocation denied (geoMockEnabled=false)", () => {
    // geoMockEnabled stays false → useGeoCityDefault mock never fires onCity
    render(<ScheduleStartPage />);
    const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
    expect(depInput.defaultValue).toBe("");
  });

  it("4.1.1-R8: does not override departure when cross-section store has schedule filter", () => {
    setScheduleFilter({
      mode: "route", departure: "LED", arrival: "KGD",
      dateFrom: "20260515", dateTo: "20260521",
      timeFrom: "0000", timeTo: "2400",
      onlyDirect: false, showReturn: false, searchExecuted: false,
    });
    geoMockEnabled = true;
    render(<ScheduleStartPage />);
    // Departure should be LED from the snapshot, NOT overridden by geo.
    const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
    expect(depInput.defaultValue).toBe("LED");
  });

  it("4.1.1-R10: time default = 00:00-24:00 on all viewports", () => {
    render(<ScheduleStartPage />);
    const timeSelector = screen.getByTestId("time-selector");
    expect(timeSelector.textContent).toContain("00:00");
    expect(timeSelector.textContent).toContain("24:00");
  });
});
