/**
 * @vitest-environment jsdom
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { FlightsMapFilter } from "./FlightsMapFilter.js";
import type { IFlightsMapFilterState } from "../types.js";

// Capture props passed to PrimeReact Calendar.
let lastCalendarProps: Record<string, unknown> | null = null;
vi.mock("primereact/calendar", () => ({
  Calendar: (props: Record<string, unknown>) => {
    lastCalendarProps = props;
    return <div data-testid="fm-date-input" />;
  },
}));

// Stub the composite CityAutocomplete so rendering is cheap.
vi.mock("@/ui/city-autocomplete/index.js", () => ({
  CityAutocomplete: (props: Record<string, unknown>) => (
    <input
      data-testid={`${(props["testIdPrefix"] as string) ?? "cac"}-input`}
      placeholder={props["placeholder"] as string}
    />
  ),
  SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
    <button type="button" data-testid={props.testId} onClick={props.onClick} />
  ),
}));

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

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

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({
    t: (key: string) =>
      key === "SHARED.TODAY"
        ? "Сегодня"
        : key === "SHARED.TOMORROW"
          ? "Завтра"
          : key,
    i18n: { language: "ru" },
  }),
}));

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

function yyyymmdd(d: Date): string {
  const y = d.getFullYear().toString();
  const m = (d.getMonth() + 1).toString().padStart(2, "0");
  const day = d.getDate().toString().padStart(2, "0");
  return `${y}${m}${day}`;
}

function addDays(base: Date, n: number): Date {
  const d = new Date(base);
  d.setDate(d.getDate() + n);
  d.setHours(0, 0, 0, 0);
  return d;
}

describe("FlightsMapFilter — Calendar wiring", () => {
  beforeEach(() => {
    lastCalendarProps = null;
  });

  it("passes minDate and maxDate to Calendar", () => {
    const onChange = vi.fn();
    render(<FlightsMapFilter value={filter()} onChange={onChange} />);

    const min = lastCalendarProps!["minDate"] as Date;
    const max = lastCalendarProps!["maxDate"] as Date;

    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const expectedMin = addDays(today, -1);
    const expectedMax = new Date(today);
    expectedMax.setMonth(expectedMax.getMonth() + 6);

    expect(min.getTime()).toBe(expectedMin.getTime());
    expect(max.getTime()).toBe(expectedMax.getTime());
  });

  it("uses the provided SSR today anchor for date-only calendar bounds", () => {
    const onChange = vi.fn();
    render(
      <FlightsMapFilter
        value={filter()}
        today="20260506"
        onChange={onChange}
      />,
    );

    const min = lastCalendarProps!["minDate"] as Date;
    const max = lastCalendarProps!["maxDate"] as Date;

    expect(min.toISOString()).toBe(new Date(2026, 4, 5).toISOString());
    expect(max.toISOString()).toBe(new Date(2026, 10, 6).toISOString());
    expect(min.getHours()).toBe(0);
    expect(min.getMinutes()).toBe(0);
  });

  it("disables every date when availableDays is empty", () => {
    const onChange = vi.fn();
    render(
      <FlightsMapFilter value={filter()} availableDays={[]} onChange={onChange} />,
    );

    const disabled = lastCalendarProps!["disabledDates"] as Date[];
    expect(disabled.length).toBeGreaterThan(180);
  });

  it("excludes available days from disabledDates", () => {
    const onChange = vi.fn();
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const plusTwo = addDays(today, 2);

    render(
      <FlightsMapFilter
        value={filter()}
        availableDays={[yyyymmdd(plusTwo)]}
        onChange={onChange}
      />,
    );

    const disabled = lastCalendarProps!["disabledDates"] as Date[];
    const contains = disabled.some((d) => {
      const x = new Date(d);
      x.setHours(0, 0, 0, 0);
      return x.getTime() === plusTwo.getTime();
    });
    expect(contains).toBe(false);
  });

  it("snaps a disabled value.date forward to the next available day", () => {
    const onChange = vi.fn();
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const plusOne = addDays(today, 1);
    const plusTwo = addDays(today, 2);

    render(
      <FlightsMapFilter
        value={filter({ date: yyyymmdd(plusOne) })}
        availableDays={[yyyymmdd(plusTwo)]}
        onChange={onChange}
      />,
    );

    expect(onChange).toHaveBeenCalled();
    const called = onChange.mock.calls[0]![0] as IFlightsMapFilterState;
    expect(called.date).toBe(yyyymmdd(plusTwo));
  });

  it("clears value.date when no enabled date exists in the window", () => {
    const onChange = vi.fn();
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const plusOne = addDays(today, 1);

    render(
      <FlightsMapFilter
        value={filter({ date: yyyymmdd(plusOne) })}
        availableDays={[]}
        onChange={onChange}
      />,
    );

    expect(onChange).toHaveBeenCalled();
    const called = onChange.mock.calls[0]![0] as IFlightsMapFilterState;
    expect(called.date).toBeUndefined();
  });

  it("does not snap when the current date is already enabled", () => {
    const onChange = vi.fn();
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const plusTwo = addDays(today, 2);

    render(
      <FlightsMapFilter
        value={filter({ date: yyyymmdd(plusTwo) })}
        availableDays={[yyyymmdd(plusTwo)]}
        onChange={onChange}
      />,
    );

    expect(onChange).not.toHaveBeenCalled();
  });
});

describe("FlightsMapFilter — TIRREDESIGN-22: calendar today/tomorrow labels", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date(2026, 4, 6, 12, 0, 0));
    lastCalendarProps = null;
  });

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

  it("formats today's selected date as a word", () => {
    render(
      <FlightsMapFilter
        value={filter({ departure: "MOW", date: "20260506" })}
        onChange={vi.fn()}
      />,
    );

    const formatDateTime = lastCalendarProps!["formatDateTime"] as (
      date: Date,
    ) => string;

    expect(formatDateTime(new Date(2026, 4, 6))).toBe("Сегодня");
  });

  it("formats tomorrow's selected date as a word", () => {
    render(
      <FlightsMapFilter
        value={filter({ departure: "MOW", date: "20260507" })}
        onChange={vi.fn()}
      />,
    );

    const formatDateTime = lastCalendarProps!["formatDateTime"] as (
      date: Date,
    ) => string;

    expect(formatDateTime(new Date(2026, 4, 7))).toBe("Завтра");
  });

  it("keeps normal dates in dd.MM.yyyy format", () => {
    render(
      <FlightsMapFilter
        value={filter({ departure: "MOW", date: "20260508" })}
        onChange={vi.fn()}
      />,
    );

    const formatDateTime = lastCalendarProps!["formatDateTime"] as (
      date: Date,
    ) => string;

    expect(formatDateTime(new Date(2026, 4, 8))).toBe("08.05.2026");
  });

  it("parses typed today/tomorrow words back to dates", () => {
    render(
      <FlightsMapFilter
        value={filter({ departure: "MOW", date: "20260506" })}
        onChange={vi.fn()}
      />,
    );

    const parseDateTime = lastCalendarProps!["parseDateTime"] as (
      text: string,
    ) => Date;

    expect(parseDateTime("Сегодня").toISOString()).toBe(
      new Date(2026, 4, 6).toISOString(),
    );
    expect(parseDateTime("Завтра").toISOString()).toBe(
      new Date(2026, 4, 7).toISOString(),
    );
  });
});

// ---------------------------------------------------------------------------
// TZ §4.1.24.2 R16: Calendar disabled when Город вылета is empty
// ---------------------------------------------------------------------------

describe("FlightsMapFilter — §4.1.24.2 R16: date picker disabled when no departure", () => {
  beforeEach(() => {
    lastCalendarProps = null;
  });

  it("4.1.24-R16: Calendar disabled=true when departure is not set", () => {
    const onChange = vi.fn();
    render(<FlightsMapFilter value={filter()} onChange={onChange} />);
    expect(lastCalendarProps!["disabled"]).toBe(true);
  });

  it("4.1.24-R16: Calendar disabled=false when departure is set", () => {
    const onChange = vi.fn();
    render(
      <FlightsMapFilter value={filter({ departure: "MOW" })} onChange={onChange} />,
    );
    expect(lastCalendarProps!["disabled"]).toBe(false);
  });

  it("4.1.24-R16: day-quick-pick buttons disabled when departure is not set", () => {
    const onChange = vi.fn();
    render(<FlightsMapFilter value={filter()} onChange={onChange} />);
    const btns = screen.getAllByTestId(/^day-quick-pick-/);
    for (const btn of btns) {
      expect((btn as HTMLButtonElement).disabled).toBe(true);
    }
  });

  it("4.1.24-R16: day-quick-pick buttons enabled when departure is set", () => {
    const onChange = vi.fn();
    render(
      <FlightsMapFilter value={filter({ departure: "MOW" })} onChange={onChange} />,
    );
    const btns = screen.getAllByTestId(/^day-quick-pick-/);
    for (const btn of btns) {
      expect((btn as HTMLButtonElement).disabled).toBe(false);
    }
  });
});

// ---------------------------------------------------------------------------
// TZ §4.1.24.2 R13/R14: Domestic/international toggles disabled without departure
// ---------------------------------------------------------------------------

describe("FlightsMapFilter — §4.1.24.2 R13/R14: toggle disabled states", () => {
  it("4.1.24-R13: domestic toggle disabled when departure empty", () => {
    const onChange = vi.fn();
    render(<FlightsMapFilter value={filter()} onChange={onChange} />);
    const toggle = screen.getByTestId("fm-domestic-toggle") as HTMLInputElement;
    expect(toggle.disabled).toBe(true);
  });

  it("4.1.24-R14: international toggle disabled when departure empty", () => {
    const onChange = vi.fn();
    render(<FlightsMapFilter value={filter()} onChange={onChange} />);
    const toggle = screen.getByTestId("fm-international-toggle") as HTMLInputElement;
    expect(toggle.disabled).toBe(true);
  });

  it("4.1.24-R13: domestic toggle enabled when departure is set", () => {
    const onChange = vi.fn();
    render(
      <FlightsMapFilter value={filter({ departure: "MOW" })} onChange={onChange} />,
    );
    const toggle = screen.getByTestId("fm-domestic-toggle") as HTMLInputElement;
    expect(toggle.disabled).toBe(false);
  });

  it("4.1.24-R14: international toggle enabled when departure is set", () => {
    const onChange = vi.fn();
    render(
      <FlightsMapFilter value={filter({ departure: "MOW" })} onChange={onChange} />,
    );
    const toggle = screen.getByTestId("fm-international-toggle") as HTMLInputElement;
    expect(toggle.disabled).toBe(false);
  });

  it("4.1.24-R13: handleDomesticChange disables international when domestic is enabled", () => {
    // Verify the mutual-exclusion logic by calling the handler directly.
    // A controlled <input type="checkbox"> onChange in React 18/jsdom requires
    // simulating the nativeEvent.target.checked property via fireEvent.click.
    // This test verifies the business logic: enabling domestic → international=false.
    const onChange = vi.fn();
    const { rerender } = render(
      <FlightsMapFilter
        value={filter({ departure: "MOW", domestic: false, international: true })}
        onChange={onChange}
      />,
    );
    const toggle = screen.getByTestId("fm-domestic-toggle") as HTMLInputElement;
    // Verify the toggle is enabled (departure is set)
    expect(toggle.disabled).toBe(false);
    // fireEvent.click simulates user clicking an unchecked checkbox → checked becomes true
    fireEvent.click(toggle);
    expect(onChange).toHaveBeenCalledWith(
      expect.objectContaining({ domestic: true, international: false }),
    );
    void rerender; // suppress lint
  });

  it("4.1.24-R14: handleInternationalChange disables domestic when international is enabled", () => {
    const onChange = vi.fn();
    render(
      <FlightsMapFilter
        value={filter({ departure: "MOW", domestic: true, international: false })}
        onChange={onChange}
      />,
    );
    const toggle = screen.getByTestId("fm-international-toggle") as HTMLInputElement;
    expect(toggle.disabled).toBe(false);
    fireEvent.click(toggle);
    expect(onChange).toHaveBeenCalledWith(
      expect.objectContaining({ international: true, domestic: false }),
    );
  });
});

// ---------------------------------------------------------------------------
// TZ §4.1.24.1 R7/R8/R19: filter header label and no-collapse
// ---------------------------------------------------------------------------

describe("FlightsMapFilter — §4.1.24.1/§4.1.24.2 structural assertions", () => {
  it("4.1.24-R8: no collapse/expand toggle button in the filter", () => {
    render(<FlightsMapFilter value={filter()} onChange={vi.fn()} />);
    // There should be no accordion-style toggle inside the filter container
    const collapse = document.querySelector(".flights-map-filter .collapse-toggle, .flights-map-filter .accordion-toggle");
    expect(collapse).toBeNull();
  });

  it("4.1.24-R19: hint text rendered inside the filter", () => {
    render(<FlightsMapFilter value={filter()} onChange={vi.fn()} />);
    // The hint text i18n key is FLIGHTS-MAP.FILTER_INFO; mock returns the key
    expect(screen.getByText("FLIGHTS-MAP.FILTER_INFO")).toBeTruthy();
  });

  it("4.1.24-R11: swap button is rendered", () => {
    render(<FlightsMapFilter value={filter()} onChange={vi.fn()} />);
    const swapBtn = screen.getByTestId("fm-exchange-btn");
    expect(swapBtn).toBeTruthy();
  });

  it("4.1.24-R11: clicking swap exchanges departure and arrival", () => {
    const onChange = vi.fn();
    render(
      <FlightsMapFilter
        value={filter({ departure: "MOW", arrival: "LED" })}
        onChange={onChange}
      />,
    );
    const swapBtn = screen.getByTestId("fm-exchange-btn");
    fireEvent.click(swapBtn);
    expect(onChange).toHaveBeenCalledWith(
      expect.objectContaining({ departure: "LED", arrival: "MOW" }),
    );
  });
});
