/**
 * Tests for ScheduleFilter — clear-button (X) behaviour per TZ §4.1.9
 * Tables 13/14.
 *
 * @vitest-environment jsdom
 */

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

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

const mockNavigate = vi.fn();
const scheduleCalendarMock = 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 }),
}));

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

// PrimeReact Calendar stub — read-only input so state is driven by props
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 per testId so tests can fire events.
// The last-mounted Slider's onChange is tracked; for the return-time slider tests
// we render with returnFlights=true so the second Slider mounts last.
let _sliderOnChange: ((e: { value: number[] }) => void) | null = null;
// All mounted sliders — keyed by render order; index 0 = outbound, 1 = return
const _sliderOnChanges: Array<((e: { value: number[] }) => void) | null> = [];

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

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} />
  ),
}));

vi.mock("@/shared/dateWindow.js", () => ({
  scheduleWindowBounds: () => {
    const min = new Date(2026, 0, 1);
    const max = new Date(2026, 11, 31);
    return [min, max];
  },
}));

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

describe("ScheduleFilter – clear-button (X) per TZ §4.1.9 Tables 13/14", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    scheduleCalendarMock.days = [];
    scheduleCalendarMock.loaded = false;
    scheduleCalendarMock.params = [];
    _sliderOnChange = null;
    _sliderOnChanges.length = 0;
  });

  // -------------------------------------------------------------------------
  // Outbound date range
  // -------------------------------------------------------------------------

  it("4.1.9-S1: schedule date-range X is hidden when no dates are selected", () => {
    render(<ScheduleFilter />);
    expect(screen.queryByTestId("schedule-date-clear")).toBeNull();
  });

  it("4.1.9-S2: schedule date-range X is present when both dates are provided", () => {
    render(
      <ScheduleFilter initialDateFrom="20260601" initialDateTo="20260607" />,
    );
    expect(screen.queryByTestId("schedule-date-clear")).toBeTruthy();
  });

  it("4.1.9-S3: clicking schedule date-range X clears the range and hides X", () => {
    render(
      <ScheduleFilter initialDateFrom="20260601" initialDateTo="20260607" />,
    );
    const clearBtn = screen.queryByTestId("schedule-date-clear")!;
    fireEvent.click(clearBtn);
    expect(screen.queryByTestId("schedule-date-clear")).toBeNull();
  });

  it("4.1.9-S4: schedule date-range clear X has aria-label for a11y", () => {
    render(
      <ScheduleFilter initialDateFrom="20260601" initialDateTo="20260607" />,
    );
    expect(
      screen.queryByTestId("schedule-date-clear")!.getAttribute("aria-label"),
    ).toBe("SHARED.A11Y-CLEAR");
  });

  // -------------------------------------------------------------------------
  // Return date range (only visible when `initialReturnFlights` = true)
  // -------------------------------------------------------------------------

  it("4.1.9-S5: return date-range X is hidden when no return dates are selected", () => {
    render(<ScheduleFilter initialReturnFlights={true} />);
    expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
  });

  it("4.1.9-S6: return date-range X is present when return dates are provided", () => {
    render(
      <ScheduleFilter
        initialReturnFlights={true}
        initialReturnDateFrom="20260608"
        initialReturnDateTo="20260614"
      />,
    );
    expect(screen.queryByTestId("schedule-return-date-clear")).toBeTruthy();
  });

  it("4.1.9-S7: clicking return date-range X clears the range and hides X", () => {
    render(
      <ScheduleFilter
        initialReturnFlights={true}
        initialReturnDateFrom="20260608"
        initialReturnDateTo="20260614"
      />,
    );
    const clearBtn = screen.queryByTestId("schedule-return-date-clear")!;
    fireEvent.click(clearBtn);
    expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
  });

  it("4.1.9-S8: return date-range clear X has aria-label for a11y", () => {
    render(
      <ScheduleFilter
        initialReturnFlights={true}
        initialReturnDateFrom="20260608"
        initialReturnDateTo="20260614"
      />,
    );
    expect(
      screen.queryByTestId("schedule-return-date-clear")!.getAttribute("aria-label"),
    ).toBe("SHARED.A11Y-CLEAR");
  });

  // -------------------------------------------------------------------------
  // Sliders — no X required (TZ Table 12/14 exclusion)
  // -------------------------------------------------------------------------

  it("4.1.9-S9: no clear button is rendered for the departure-time slider", () => {
    render(<ScheduleFilter />);
    // Time selector is present
    expect(screen.queryByTestId("time-selector")).toBeTruthy();
    // But no slider clear button
    expect(screen.queryByTestId("time-slider-clear")).toBeNull();
  });
});

describe("ScheduleFilter – time slider 1h minimum gap per TZ §4.1.9 Table 14", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    scheduleCalendarMock.days = [];
    scheduleCalendarMock.loaded = false;
    scheduleCalendarMock.params = [];
    _sliderOnChange = null;
    _sliderOnChanges.length = 0;
  });

  it("4.1.9-R: outbound time slider enforces 1h minimum gap (zero gap → to clamped to from + 60)", () => {
    render(<ScheduleFilter initialTimeFrom="0600" initialTimeTo="0700" />);
    act(() => {
      _sliderOnChanges[0]?.({ value: [600, 600] });
    });
    const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
    expect(value?.textContent).toBe("10:00 — 11:00");
  });

  it("4.1.9-R: outbound time slider cannot set timeTo before timeFrom (inverted range normalised)", () => {
    render(<ScheduleFilter />);
    act(() => {
      _sliderOnChanges[0]?.({ value: [800, 600] });
    });
    const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
    expect(value?.textContent).toBe("10:00 — 13:20");
  });

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

  it("4.1.9-R: return time slider enforces 1h minimum gap", () => {
    render(<ScheduleFilter initialReturnFlights={true} />);
    // _sliderOnChanges[0] = outbound, _sliderOnChanges[1] = return
    act(() => {
      _sliderOnChanges[1]?.({ value: [720, 720] });
    });
    const value = screen.getByTestId("return-time-selector").querySelector(".time-selector__value");
    expect(value?.textContent).toBe("12:00 — 13:00");
  });
});

describe("ScheduleFilter – operating days calendar parity (TIRREDESIGN-12)", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    scheduleCalendarMock.days = [];
    scheduleCalendarMock.loaded = false;
    scheduleCalendarMock.params = [];
    _sliderOnChange = null;
    _sliderOnChanges.length = 0;
  });

  it("requests schedule days from the calendar minimum date", () => {
    scheduleCalendarMock.days = ["2026-01-02"];
    scheduleCalendarMock.loaded = true;

    render(
      <ScheduleFilter
        initialDeparture="LED"
        initialArrival="KUF"
        initialDirectOnly={true}
      />,
    );

    expect(scheduleCalendarMock.params[0]).toMatchObject({
      date: "2026-01-01",
      departure: "LED",
      arrival: "KUF",
      connections: false,
    });
  });

  it("disables dates that are missing from the loaded operating-days response", () => {
    scheduleCalendarMock.days = ["2026-01-02"];
    scheduleCalendarMock.loaded = true;

    render(<ScheduleFilter initialDeparture="LED" initialArrival="KUF" />);

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

    expect(disabledDates).toContain("2026-01-01");
    expect(disabledDates).not.toContain("2026-01-02");
    expect(disabledDates).toContain("2026-01-03");
  });

  it("treats a loaded empty operating-days response as no available dates", () => {
    scheduleCalendarMock.days = [];
    scheduleCalendarMock.loaded = true;

    render(<ScheduleFilter initialDeparture="LED" initialArrival="KUF" />);

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

    expect(disabledDates).toContain("2026-01-01");
    expect(disabledDates).toContain("2026-01-02");
  });
});

describe("ScheduleFilter – validation per TZ §4.1.9.4", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    scheduleCalendarMock.days = [];
    scheduleCalendarMock.loaded = false;
    scheduleCalendarMock.params = [];
    _sliderOnChange = null;
    _sliderOnChanges.length = 0;
  });

  // -------------------------------------------------------------------------
  // §4.1.9.4 range ≤ 7 days
  // -------------------------------------------------------------------------

  it("4.1.9.4-R: outbound range > 7 days shows validation error + blocks submit", () => {
    render(
      <ScheduleFilter
        initialDeparture="SVO"
        initialArrival="LED"
        initialDateFrom="20260601"
        initialDateTo="20260611"
      />,
    );
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(screen.queryByTestId("schedule-range-error")).toBeTruthy();
    expect(mockNavigate).not.toHaveBeenCalled();
  });

  it("4.1.9.4-R: outbound range exactly 7 days passes validation", () => {
    render(
      <ScheduleFilter
        initialDeparture="SVO"
        initialArrival="LED"
        initialDateFrom="20260601"
        initialDateTo="20260608"
      />,
    );
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(screen.queryByTestId("schedule-range-error")).toBeNull();
    expect(mockNavigate).toHaveBeenCalled();
  });

  it("4.1.9.4-R: range error clears when user picks a new date range", () => {
    render(
      <ScheduleFilter
        initialDeparture="SVO"
        initialArrival="LED"
        initialDateFrom="20260601"
        initialDateTo="20260611"
      />,
    );
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(screen.queryByTestId("schedule-range-error")).toBeTruthy();

    // Simulate the Calendar onChange (stub fires the onChange prop)
    const calInput = screen.getByTestId("schedule-date-input");
    fireEvent.change(calInput, { target: {} });
    // The error is cleared on the Calendar's onChange in ScheduleFilter —
    // but the stub doesn't call onChange. Instead clear via the X button.
    const clearBtn = screen.queryByTestId("schedule-date-clear");
    if (clearBtn) {
      fireEvent.click(clearBtn);
      expect(screen.queryByTestId("schedule-range-error")).toBeNull();
    }
  });

  // -------------------------------------------------------------------------
  // §4.1.9.4 return date must be >= outbound dateTo
  // -------------------------------------------------------------------------

  it("4.1.9.4-R: return date before outbound dateTo auto-clears on mount (Table 16)", () => {
    render(
      <ScheduleFilter
        initialDeparture="SVO"
        initialArrival="LED"
        initialDateFrom="20260601"
        initialDateTo="20260607"
        initialReturnFlights={true}
        initialReturnDateFrom="20260603"
        initialReturnDateTo="20260610"
      />,
    );
    // TZ §4.1.9.4 Table 16: an invalid retFrom < outTo combination is
    // blanked before the user can even submit, rather than only raising
    // an error after submit. The return X button is therefore hidden.
    expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
    expect(screen.queryByTestId("schedule-return-before-outbound-error")).toBeNull();
  });

  it("4.1.9.4-R: return date equal to outbound dateTo passes validation", () => {
    render(
      <ScheduleFilter
        initialDeparture="SVO"
        initialArrival="LED"
        initialDateFrom="20260601"
        initialDateTo="20260607"
        initialReturnFlights={true}
        initialReturnDateFrom="20260607"
        initialReturnDateTo="20260614"
      />,
    );
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(screen.queryByTestId("schedule-return-before-outbound-error")).toBeNull();
    expect(mockNavigate).toHaveBeenCalled();
  });

  it("4.1.9.4-R: return date after outbound dateTo passes validation", () => {
    render(
      <ScheduleFilter
        initialDeparture="SVO"
        initialArrival="LED"
        initialDateFrom="20260601"
        initialDateTo="20260607"
        initialReturnFlights={true}
        initialReturnDateFrom="20260608"
        initialReturnDateTo="20260614"
      />,
    );
    fireEvent.submit(screen.getByTestId("search-form"));
    expect(screen.queryByTestId("schedule-return-before-outbound-error")).toBeNull();
    expect(mockNavigate).toHaveBeenCalled();
  });

  it("4.1.9.4-R: clearing outbound also clears return (Table 16 cascade)", () => {
    render(
      <ScheduleFilter
        initialDeparture="SVO"
        initialArrival="LED"
        initialDateFrom="20260601"
        initialDateTo="20260607"
        initialReturnFlights={true}
        initialReturnDateFrom="20260608"
        initialReturnDateTo="20260614"
      />,
    );
    // Sanity: both ranges are populated.
    expect(screen.queryByTestId("schedule-date-clear")).toBeTruthy();
    expect(screen.queryByTestId("schedule-return-date-clear")).toBeTruthy();

    // Clicking the outbound X must clear both ranges.
    fireEvent.click(screen.getByTestId("schedule-date-clear"));
    expect(screen.queryByTestId("schedule-date-clear")).toBeNull();
    expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
  });
});
