import { describe, expect, it, vi } from "vitest";
import { render, screen } from "~/test-utils.js";
import userEvent from "@testing-library/user-event";
import { DateInput } from "./DateInput.js";

function setup(props: Partial<React.ComponentProps<typeof DateInput>> = {}) {
  const onChange = props.onChange ?? vi.fn();
  const utils = render(<DateInput value={props.value ?? ""} onChange={onChange} {...props} />);
  const textInput = screen.getByPlaceholderText("dd/mm/yyyy");
  return { ...utils, textInput, onChange };
}

describe("DateInput", () => {
  describe("initial display", () => {
    it("renders an empty text input when value is empty", () => {
      const { textInput } = setup({ value: "" });
      expect(textInput).toHaveValue("");
    });

    it("converts an ISO value to dd/mm/yyyy display format", () => {
      const { textInput } = setup({ value: "2024-03-15" });
      expect(textInput).toHaveValue("15/03/2024");
    });

    it("renders the calendar button", () => {
      setup();
      expect(screen.getByRole("button", { name: /open date picker/i })).toBeInTheDocument();
    });
  });

  describe("text input – autoSlash formatting", () => {
    it("formats digits as dd/mm/yyyy while typing", async () => {
      const user = userEvent.setup();
      const { textInput } = setup();
      await user.type(textInput, "15032024");
      expect(textInput).toHaveValue("15/03/2024");
    });

    it("inserts first slash after two digits", async () => {
      const user = userEvent.setup();
      const { textInput } = setup();
      await user.type(textInput, "15");
      expect(textInput).toHaveValue("15");
      await user.type(textInput, "0");
      expect(textInput).toHaveValue("15/0");
    });

    it("inserts second slash after four digits", async () => {
      const user = userEvent.setup();
      const { textInput } = setup();
      await user.type(textInput, "1503");
      expect(textInput).toHaveValue("15/03");
      await user.type(textInput, "2");
      expect(textInput).toHaveValue("15/03/2");
    });

    it("calls onChange with ISO string once all 8 digits are entered", async () => {
      const user = userEvent.setup();
      const { textInput, onChange } = setup();
      await user.type(textInput, "15032024");
      expect(onChange).toHaveBeenLastCalledWith("2024-03-15");
    });

    it("does NOT call onChange while input is still incomplete", async () => {
      const user = userEvent.setup();
      const { textInput, onChange } = setup();
      await user.type(textInput, "1503");
      // The partial value is not valid – onChange should not have been called with an ISO string
      expect(onChange).not.toHaveBeenCalledWith(expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/));
    });
  });

  describe("validation – invalid dates are rejected", () => {
    it("does not call onChange for a day > 31", async () => {
      const user = userEvent.setup();
      const { textInput, onChange } = setup();
      // Day 32 is invalid
      await user.type(textInput, "32032024");
      expect(onChange).not.toHaveBeenCalled();
    });

    it("does not call onChange for month > 12", async () => {
      const user = userEvent.setup();
      const { textInput, onChange } = setup();
      await user.type(textInput, "01132024");
      expect(onChange).not.toHaveBeenCalled();
    });

    it("does not call onChange for year < 1900", async () => {
      const user = userEvent.setup();
      const { textInput, onChange } = setup();
      await user.type(textInput, "01011899");
      expect(onChange).not.toHaveBeenCalled();
    });

    it("does not call onChange for year > 2100", async () => {
      const user = userEvent.setup();
      const { textInput, onChange } = setup();
      await user.type(textInput, "01012101");
      expect(onChange).not.toHaveBeenCalled();
    });
  });

  describe("blur normalisation", () => {
    it("re-normalises the display to canonical dd/mm/yyyy on blur", async () => {
      const user = userEvent.setup();
      const { textInput } = setup();
      await user.type(textInput, "15032024");
      await user.tab(); // triggers blur
      expect(textInput).toHaveValue("15/03/2024");
    });

    it("keeps the incomplete value on blur so the user can continue editing", async () => {
      const user = userEvent.setup();
      const { textInput } = setup();
      await user.type(textInput, "1503");
      await user.tab();
      // Incomplete input – should remain as-is
      expect(textInput).toHaveValue("15/03");
    });

    it("clears display to empty on blur when the field is empty", async () => {
      const user = userEvent.setup();
      const { textInput } = setup({ value: "2024-03-15" });
      await user.clear(textInput);
      await user.tab();
      expect(textInput).toHaveValue("");
    });
  });

  describe("external value changes", () => {
    it("updates display when the value prop changes", () => {
      const { rerender, textInput } = setup({ value: "2024-01-01" });
      expect(textInput).toHaveValue("01/01/2024");
      rerender(<DateInput value="2025-12-31" onChange={vi.fn()} />);
      expect(textInput).toHaveValue("31/12/2025");
    });

    it("clears display when value prop is reset to empty string", () => {
      const { rerender, textInput } = setup({ value: "2024-01-01" });
      rerender(<DateInput value="" onChange={vi.fn()} />);
      expect(textInput).toHaveValue("");
    });
  });

  describe("accessibility and props", () => {
    it("forwards the id prop to the text input", () => {
      setup({ id: "my-date" });
      expect(screen.getByPlaceholderText("dd/mm/yyyy")).toHaveAttribute("id", "my-date");
    });

    it("sets the required attribute when required is true", () => {
      const { textInput } = setup({ required: true });
      expect(textInput).toBeRequired();
    });

    it("disables both the text input and the calendar button when disabled", () => {
      setup({ disabled: true });
      expect(screen.getByPlaceholderText("dd/mm/yyyy")).toBeDisabled();
      expect(screen.getByRole("button", { name: /open date picker/i })).toBeDisabled();
    });

    it("hides the native date input from assistive technology", () => {
      const { container } = setup();
      const nativeDateInput = container.querySelector('input[type="date"]');
      expect(nativeDateInput).toHaveAttribute("aria-hidden", "true");
    });
  });

  describe("edge cases", () => {
    it("strips non-digit characters typed into the input (autoSlash rejects them)", async () => {
      const user = userEvent.setup();
      const { textInput } = setup();
      await user.type(textInput, "ab/cd/efgh");
      // autoSlash strips all non-digits, so nothing should remain
      expect(textInput).toHaveValue("");
    });

    it("returns an ISO string for a leap year Feb 29 (29/02/2024)", async () => {
      const user = userEvent.setup();
      const { textInput, onChange } = setup();
      await user.type(textInput, "29022024");
      // displayToISO only validates numeric ranges (day 1-31, month 1-12, year 1900-2100)
      // it does NOT check calendar correctness, so this passes and calls onChange
      expect(textInput).toHaveValue("29/02/2024");
      expect(onChange).toHaveBeenLastCalledWith("2024-02-29");
    });

    it("returns an ISO string for Feb 29 on a non-leap year (29/02/2023) — no calendar validation", async () => {
      const user = userEvent.setup();
      const { textInput, onChange } = setup();
      await user.type(textInput, "29022023");
      // displayToISO does not validate calendar correctness — it only checks range bounds
      expect(textInput).toHaveValue("29/02/2023");
      expect(onChange).toHaveBeenLastCalledWith("2023-02-29");
    });

    it("returns an ISO string for day 31 on a 30-day month (31/04/2024) — no calendar validation", async () => {
      const user = userEvent.setup();
      const { textInput, onChange } = setup();
      await user.type(textInput, "31042024");
      // displayToISO only rejects day > 31 or month > 12, not calendar-invalid combos
      expect(textInput).toHaveValue("31/04/2024");
      expect(onChange).toHaveBeenLastCalledWith("2024-04-31");
    });
  });
});
