/**
 * Tests for OnlineBoardFilter — clear-button (X) behaviour per TZ §4.1.9
 * Tables 11/12.
 *
 * @vitest-environment jsdom
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, act } from "@testing-library/react";
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";

// ---------------------------------------------------------------------------
// Module mocks
// ---------------------------------------------------------------------------

const mockNavigate = vi.fn();
const calendarDaysMock = vi.hoisted(() => ({
  days: [] as string[],
  loaded: false,
  params: [] as unknown[],
}));

vi.mock("@modern-js/runtime/router", () => ({
  useNavigate: () => mockNavigate,
  useParams: () => ({ lang: "ru-ru" }),
}));

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

vi.mock("@/i18n/useLocale.js", () => ({
  useLocale: () => ({ locale: "ru-ru", language: "ru" }),
}));

vi.mock("@/shared/dictionaries/index.js", () => ({
  useDictionaries: () => ({ dictionaries: null, loading: false, error: null }),
}));

// useCalendarDays would otherwise pull the api provider into the test
// tree just for the disabled-dates wiring added in TIRREDESIGN-12.
vi.mock("../hooks/useCalendarDays.js", () => ({
  useCalendarDays: (params: unknown) => {
    calendarDaysMock.params.push(params);
    return {
      days: calendarDaysMock.days,
      loading: false,
      loaded: calendarDaysMock.loaded,
    };
  },
}));

vi.mock("@/shared/state/crossSectionNavigation.js", () => ({
  setBoardFilter: vi.fn(),
}));

// PrimeReact Calendar is stubbed so tests don't require a DOM calendar widget.
vi.mock("primereact/calendar", () => ({
  Calendar: (props: Record<string, unknown>) => {
    const inputRef = props["inputRef"] as React.RefObject<HTMLInputElement> | undefined;
    const formatLocalYmd = (d: Date) =>
      `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
    return (
      <input
        data-testid={props["data-testid"] as string}
        placeholder={props["placeholder"] as string}
        data-disabled-dates={((props["disabledDates"] as Date[] | undefined) ?? [])
          .map(formatLocalYmd)
          .join(",")}
        readOnly
        ref={inputRef}
      />
    );
  },
}));

// PrimeReact Slider stub — captures onChange so tests can fire range events
let _sliderOnChange: ((e: { value: number[] }) => void) | null = null;

vi.mock("primereact/slider", () => ({
  Slider: (props: Record<string, unknown>) => {
    _sliderOnChange = props["onChange"] as (e: { value: number[] }) => void;
    return <div data-testid="slider-stub" />;
  },
}));

// DayQuickPick — not needed for these tests
vi.mock("@/ui/calendar/DayQuickPick.js", () => ({
  DayQuickPick: () => null,
}));

vi.mock("@/ui/city-autocomplete/index.js", () => ({
  CityAutocomplete: (props: Record<string, unknown>) => (
    <input
      data-testid={`${(props["testIdPrefix"] as string) ?? "city-autocomplete"}-input`}
      defaultValue={(props["value"] as string) ?? ""}
    />
  ),
  SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
    <button type="button" data-testid={props.testId} onClick={props.onClick} />
  ),
}));

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function renderFlightTab() {
  render(<OnlineBoardFilter initialTab="flight" />);
}

function renderRouteTab() {
  render(<OnlineBoardFilter initialTab="route" />);
}

function resetTestState() {
  vi.clearAllMocks();
  calendarDaysMock.days = [];
  calendarDaysMock.loaded = false;
  calendarDaysMock.params = [];
  _sliderOnChange = null;
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("OnlineBoardFilter – clear-button (X) per TZ §4.1.9 Tables 11/12", () => {
  beforeEach(() => {
    resetTestState();
  });

  // -------------------------------------------------------------------------
  // Flight-number field
  // -------------------------------------------------------------------------

  it("4.1.9-R1: flight-number X is hidden when input is empty", () => {
    renderFlightTab();
    expect(screen.queryByTestId("flight-number-clear")).toBeNull();
  });

  it("4.1.9-R2: flight-number X appears when a value is typed", () => {
    renderFlightTab();
    fireEvent.change(screen.getByTestId("flight-number-input"), {
      target: { value: "1234" },
    });
    expect(screen.queryByTestId("flight-number-clear")).toBeTruthy();
  });

  it("4.1.9-R3: clicking flight-number X clears the input and hides X", () => {
    renderFlightTab();
    const input = screen.getByTestId("flight-number-input");
    fireEvent.change(input, { target: { value: "1234" } });

    const clearBtn = screen.queryByTestId("flight-number-clear")!;
    expect(clearBtn).toBeTruthy();
    fireEvent.click(clearBtn);

    expect((input as HTMLInputElement).value).toBe("");
    expect(screen.queryByTestId("flight-number-clear")).toBeNull();
  });

  it("4.1.9-R4: flight-number X has aria-label for a11y", () => {
    renderFlightTab();
    fireEvent.change(screen.getByTestId("flight-number-input"), {
      target: { value: "999" },
    });
    const clearBtn = screen.queryByTestId("flight-number-clear")!;
    expect(clearBtn.getAttribute("aria-label")).toBe("SHARED.A11Y-CLEAR");
  });

  it("4.1.9-R5: clicking X also clears any existing validation error", () => {
    renderFlightTab();
    // Submit with invalid number to trigger error
    fireEvent.change(screen.getByTestId("flight-number-input"), {
      target: { value: "abc" },
    });
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(screen.queryByTestId("flight-number-error")).toBeTruthy();

    fireEvent.click(screen.queryByTestId("flight-number-clear")!);
    expect(screen.queryByTestId("flight-number-error")).toBeNull();
  });

  // -------------------------------------------------------------------------
  // OB date (flight-tab Calendar)
  // -------------------------------------------------------------------------
  // Calendar is stubbed as a read-only input; the clear button is rendered
  // conditionally based on the `flightDate` state (a Date | null). Because
  // the Calendar stub doesn't fire an onChange, we seed the value via the
  // `initialDate` + `initialTab` props which set flightDate during init.

  it("4.1.9-R6: flight-date X is hidden when no date is selected", () => {
    renderFlightTab();
    expect(screen.queryByTestId("flight-date-clear")).toBeNull();
  });

  it("4.1.9-R7: flight-date X is present when an initial date is provided", () => {
    render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
    expect(screen.queryByTestId("flight-date-clear")).toBeTruthy();
  });

  it("4.1.9-R8: clicking flight-date X clears the date and hides X", () => {
    render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
    const clearBtn = screen.queryByTestId("flight-date-clear")!;
    expect(clearBtn).toBeTruthy();
    fireEvent.click(clearBtn);
    expect(screen.queryByTestId("flight-date-clear")).toBeNull();
  });

  it("4.1.9-R9: flight-date clear X has aria-label for a11y", () => {
    render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
    expect(screen.queryByTestId("flight-date-clear")!.getAttribute("aria-label")).toBe(
      "SHARED.A11Y-CLEAR",
    );
  });

  // -------------------------------------------------------------------------
  // Route-tab date Calendar
  // -------------------------------------------------------------------------

  it("4.1.9-R10: route-date X is hidden when no date is selected", () => {
    renderRouteTab();
    expect(screen.queryByTestId("route-date-clear")).toBeNull();
  });

  it("4.1.9-R11: route-date X is present when an initial date is provided", () => {
    render(<OnlineBoardFilter initialTab="route" initialDate="20260601" />);
    expect(screen.queryByTestId("route-date-clear")).toBeTruthy();
  });

  it("4.1.9-R12: clicking route-date X clears the date and hides X", () => {
    render(<OnlineBoardFilter initialTab="route" initialDate="20260601" />);
    const clearBtn = screen.queryByTestId("route-date-clear")!;
    fireEvent.click(clearBtn);
    expect(screen.queryByTestId("route-date-clear")).toBeNull();
  });
});

describe("OnlineBoardFilter – time slider 1h minimum gap per TZ §4.1.9 Tables 12", () => {
  beforeEach(() => {
    resetTestState();
  });

  it("4.1.9-R: time slider enforces 1h minimum gap (zero gap → to clamped to from + 60)", () => {
    render(<OnlineBoardFilter initialTab="route" initialTimeFrom="0600" initialTimeTo="0660" />);
    // Fire slider onChange with zero-gap [600, 600]
    act(() => {
      _sliderOnChange?.({ value: [600, 600] });
    });
    // The displayed time value should show from=10:00 and to=11:00
    const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
    expect(value?.textContent).toBe("10:00 — 11:00");
  });

  it("4.1.9-R: time slider cannot set timeTo before timeFrom (inverted range is normalised)", () => {
    render(<OnlineBoardFilter initialTab="route" />);
    act(() => {
      _sliderOnChange?.({ value: [800, 600] });
    });
    // After normalisation from = min(800,600)=600, to = max(800,600)=800; gap >= 60 → kept
    const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
    expect(value?.textContent).toBe("10:00 — 13:20");
  });

  it("4.1.9-R: time slider clamps from at right edge (24:00 zero gap → from = 23:00, to = 24:00)", () => {
    render(<OnlineBoardFilter initialTab="route" />);
    act(() => {
      _sliderOnChange?.({ value: [1440, 1440] });
    });
    const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
    expect(value?.textContent).toBe("23:00 — 24:00");
  });

  it("TIRREDESIGN-11: syncs the slider label when URL time params change", () => {
    const { rerender } = render(
      <OnlineBoardFilter
        initialTab="route"
        initialTimeFrom="0900"
        initialTimeTo="2100"
      />,
    );

    const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
    expect(value?.textContent).toBe("09:00 — 21:00");

    rerender(
      <OnlineBoardFilter
        initialTab="route"
        initialTimeFrom="1400"
        initialTimeTo="1800"
      />,
    );

    expect(value?.textContent).toBe("14:00 — 18:00");

    rerender(<OnlineBoardFilter initialTab="route" />);

    expect(value?.textContent).toBe("00:00 — 24:00");
  });
});

describe("OnlineBoardFilter – submit lock", () => {
  beforeEach(() => {
    resetTestState();
  });

  it("keeps the same submitted route search locked", () => {
    render(
      <OnlineBoardFilter
        initialTab="route"
        initialDeparture="MOW"
        initialArrival="LED"
        initialDate="20260515"
      />,
    );

    fireEvent.submit(screen.getByTestId("search-form"));

    expect(screen.getByTestId("search-submit")).toHaveProperty("disabled", true);
  });

  it("unlocks route search after the user changes the time range", () => {
    render(
      <OnlineBoardFilter
        initialTab="route"
        initialDeparture="MOW"
        initialArrival="LED"
        initialDate="20260515"
      />,
    );

    fireEvent.submit(screen.getByTestId("search-form"));
    expect(screen.getByTestId("search-submit")).toHaveProperty("disabled", true);

    act(() => {
      _sliderOnChange?.({ value: [600, 1440] });
    });

    expect(screen.getByTestId("search-submit")).toHaveProperty("disabled", false);
  });
});

describe("OnlineBoardFilter – operating days calendar parity (TIRREDESIGN-12)", () => {
  beforeEach(() => {
    resetTestState();
  });

  it("requests board calendar days from the calendar minimum date", () => {
    calendarDaysMock.days = ["20260505"];
    calendarDaysMock.loaded = true;

    render(
      <OnlineBoardFilter
        initialTab="route"
        initialDeparture="SVO"
        initialArrival="GDX"
        today="20260505"
      />,
    );

    expect(calendarDaysMock.params).toContainEqual({
      date: "2026-05-04",
      searchType: "route",
      departure: "SVO",
      arrival: "GDX",
    });
  });

  it("disables dates missing from a loaded board operating-days response", () => {
    calendarDaysMock.days = ["20260505"];
    calendarDaysMock.loaded = true;

    render(
      <OnlineBoardFilter
        initialTab="route"
        initialDeparture="SVO"
        initialArrival="GDX"
        today="20260505"
      />,
    );

    const disabledDates = screen
      .getByTestId("date-input")
      .getAttribute("data-disabled-dates")
      ?.split(",");

    expect(disabledDates).toContain("2026-05-04");
    expect(disabledDates).not.toContain("2026-05-05");
    expect(disabledDates).toContain("2026-05-06");
  });

  it("treats a loaded all-zero board bitmask as no available dates", () => {
    calendarDaysMock.days = [];
    calendarDaysMock.loaded = true;

    render(
      <OnlineBoardFilter
        initialTab="flight"
        initialFlightNumber="9999"
        today="20260505"
      />,
    );

    const disabledDates = screen
      .getByTestId("date-input")
      .getAttribute("data-disabled-dates")
      ?.split(",");

    expect(disabledDates).toContain("2026-05-04");
    expect(disabledDates).toContain("2026-05-05");
  });

  it("requests flight calendar days with a normalized suffix flight number", () => {
    render(
      <OnlineBoardFilter
        initialTab="flight"
        initialFlightNumber="38d"
        today="20260505"
      />,
    );

    expect(calendarDaysMock.params).toContainEqual({
      date: "2026-05-04",
      searchType: "flight",
      flightNumber: "SU0038D",
    });
  });
});

describe("OnlineBoardFilter – flight-number validation per TZ §4.1.9.3", () => {
  beforeEach(() => {
    resetTestState();
  });

  it("4.1.9.3-R: '38' submits as SU0038 in URL", () => {
    render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
    fireEvent.change(screen.getByTestId("flight-number-input"), {
      target: { value: "38" },
    });
    fireEvent.submit(screen.getByTestId("search-form"));
    // navigate should be called with a URL containing SU0038
    expect(mockNavigate).toHaveBeenCalled();
    const url = mockNavigate.mock.calls[0]![0] as string;
    expect(url).toMatch(/SU0038/);
  });

  it("4.1.9.3-R: '383' submits as SU0383 in URL", () => {
    render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
    fireEvent.change(screen.getByTestId("flight-number-input"), {
      target: { value: "383" },
    });
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(mockNavigate).toHaveBeenCalled();
    const url = mockNavigate.mock.calls[0]![0] as string;
    expect(url).toMatch(/SU0383/);
  });

  it("4.1.9.3-R: '1234' submits as SU1234 (no padding needed)", () => {
    render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
    fireEvent.change(screen.getByTestId("flight-number-input"), {
      target: { value: "1234" },
    });
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(mockNavigate).toHaveBeenCalled();
    const url = mockNavigate.mock.calls[0]![0] as string;
    expect(url).toMatch(/SU1234/);
  });

  it("TIRREDESIGN-13: suffix flight number submits as normalized SU0038D URL", () => {
    render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
    fireEvent.change(screen.getByTestId("flight-number-input"), {
      target: { value: "38d" },
    });
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(mockNavigate).toHaveBeenCalled();
    const url = mockNavigate.mock.calls[0]![0] as string;
    expect(url).toContain("/ru-ru/onlineboard/flight/SU0038D-20260601");
  });

  it("TIRREDESIGN-13: suffix flight number normalizes on blur like Angular", () => {
    render(<OnlineBoardFilter initialTab="flight" />);
    const input = screen.getByTestId("flight-number-input") as HTMLInputElement;
    fireEvent.change(input, { target: { value: "123d" } });
    fireEvent.blur(input);
    expect(input.value).toBe("0123D");
  });

  it("4.1.9.3-R: letters without digits show error and block submit", () => {
    render(<OnlineBoardFilter initialTab="flight" />);
    fireEvent.change(screen.getByTestId("flight-number-input"), {
      target: { value: "abc" },
    });
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(screen.queryByTestId("flight-number-error")).toBeTruthy();
    expect(mockNavigate).not.toHaveBeenCalled();
  });

  it("4.1.9.3-R: 5-digit flight number shows error and blocks submit", () => {
    render(<OnlineBoardFilter initialTab="flight" />);
    fireEvent.change(screen.getByTestId("flight-number-input"), {
      target: { value: "12345" },
    });
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(screen.queryByTestId("flight-number-error")).toBeTruthy();
    expect(mockNavigate).not.toHaveBeenCalled();
  });

  it("4.1.9.3-R: board calendar max date is +14 days from today", () => {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const expected = new Date(today);
    expected.setDate(today.getDate() + 14);
    // We verify the +14 window by checking that the component renders
    // without error when an initialDate 14 days out is supplied.
    const dateStr = [
      expected.getFullYear(),
      String(expected.getMonth() + 1).padStart(2, "0"),
      String(expected.getDate()).padStart(2, "0"),
    ].join("");
    expect(() =>
      render(<OnlineBoardFilter initialTab="flight" initialDate={dateStr} />),
    ).not.toThrow();
  });
});
