import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AddressLookup } from "./AddressLookup";
import { LoqateAddressLookupService } from "./LoqateAddressLookupService";

// Ensure Vitest uses the mock from the __mocks__ directory
vi.mock("./__mocks__/LoqateAddressLookupService");

const containerSearchResult = {
  Items: [
    {
      Id: "GB|RM|ENG|container-id",
      Type: "BuildingNumber",
      Text: "99 Gresham Street",
      Highlight: "0-2,3-8",
      Description: "London, EC2V 7NG - 3 Addresses",
    },
  ],
};

const subAddressResults = {
  Items: [
    {
      Id: "GB|RM|A|first",
      Type: "Address",
      Text: "First Floor",
      Description: "99 Gresham Street, London",
      Highlight: "",
    },
    {
      Id: "GB|RM|A|second",
      Type: "Address",
      Text: "Second Floor",
      Description: "99 Gresham Street, London",
      Highlight: "",
    },
  ],
};

describe("AddressLookup", () => {
  let addressLookupService: LoqateAddressLookupService;

  beforeEach(() => {
    addressLookupService = new LoqateAddressLookupService({
      baseUrl: "https://api.addressy.com/Capture/Interactive",
      apiKey: "test-api-key",
    });
  });

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

  it("renders without errors", () => {
    const { container } = render(
      <AddressLookup
        addressLookupService={addressLookupService}
        label="Find address"
      />,
    );
    expect(container).toBeInTheDocument();
  });

  it("renders a combobox", () => {
    render(
      <AddressLookup
        addressLookupService={addressLookupService}
        label="Find address"
      />,
    );
    expect(screen.getByLabelText("Find address")).toHaveRole("combobox");
  });

  it("calls address lookup service after typing 3 or more characters", async () => {
    const findSpy = vi.spyOn(addressLookupService, "search");
    render(
      <AddressLookup
        addressLookupService={addressLookupService}
        label="Find address"
        delay={0}
      />,
    );

    const input = screen.getByRole("combobox");
    await userEvent.type(input, "12");

    expect(findSpy).not.toHaveBeenCalled();

    await userEvent.type(input, "3");

    expect(findSpy).toHaveBeenCalledWith("123");
  });

  it("should display an error message when the address lookup service fails", async () => {
    vi.spyOn(addressLookupService, "search").mockRejectedValueOnce(
      new Error("Test error"),
    );
    render(
      <AddressLookup
        addressLookupService={addressLookupService}
        label="Find address"
        delay={0}
      />,
    );

    const input = screen.getByRole("combobox");
    await userEvent.type(input, "123");

    expect(await screen.findByText("An error occurred")).toBeInTheDocument();
  });

  it("should show an error message when the address retrieval fails", async () => {
    vi.spyOn(addressLookupService, "search").mockResolvedValueOnce({
      Items: [
        {
          Id: "123",
          Text: "123",
          Description: "Test address",
          Type: "Address",
          Highlight: "",
        },
      ],
    });
    vi.spyOn(addressLookupService, "get").mockRejectedValueOnce(
      new Error("Test error"),
    );
    render(
      <AddressLookup
        addressLookupService={addressLookupService}
        label="Find address"
        delay={0}
      />,
    );

    const input = screen.getByRole("combobox");
    await userEvent.type(input, "123");

    const option = await screen.findByText("123, Test address");
    userEvent.click(option);

    expect(await screen.findByText("An error occurred")).toBeInTheDocument();
  });

  it("should clear the error message when a valid address is selected", async () => {
    vi.spyOn(addressLookupService, "search")
      .mockRejectedValueOnce(new Error("Test error"))
      .mockResolvedValueOnce({
        Items: [
          {
            Id: "123",
            Text: "123",
            Description: "Test address",
            Type: "Address",
            Highlight: "",
          },
        ],
      });
    render(
      <AddressLookup
        addressLookupService={addressLookupService}
        label="Find address"
        delay={0}
      />,
    );

    // First time errors
    const input = screen.getByRole("combobox");
    await act(async () => {
      await userEvent.type(input, "123");
    });

    // Second time succeeds
    await act(async () => {
      await userEvent.type(input, "{backspace}{backspace}{backspace}");
      await userEvent.type(input, "456");
    });

    const option = await screen.findByText("123, Test address");
    await act(async () => {
      userEvent.click(option);
    });

    expect(screen.queryByText("An error occurred")).not.toBeInTheDocument();
    expect(input).not.toHaveAccessibleDescription("An error occurred");
  });

  it("should set an accessibile description when an error occurs", async () => {
    vi.spyOn(addressLookupService, "search").mockRejectedValueOnce(
      new Error("Test error"),
    );
    render(
      <AddressLookup
        addressLookupService={addressLookupService}
        label="Find address"
        delay={0}
      />,
    );

    const input = screen.getByRole("combobox");
    await act(async () => {
      await userEvent.type(input, "123");
    });

    expect(input).toHaveAccessibleDescription("An error occurred");
  });

  it("should call onError callback when an error occurs", async () => {
    const error = new Error("Test error");
    const onError = vi.fn();
    vi.spyOn(addressLookupService, "search").mockRejectedValueOnce(error);

    render(
      <AddressLookup
        addressLookupService={addressLookupService}
        label="Find address"
        delay={0}
        onError={onError}
      />,
    );

    const input = screen.getByRole("combobox");
    await userEvent.type(input, "123");

    expect(onError).toHaveBeenCalledWith(error);
  });

  describe("empty selection guard", () => {
    it("should not show an error when the input is cleared and blurred", async () => {
      vi.spyOn(addressLookupService, "search").mockResolvedValue({
        Items: [
          {
            Id: "123",
            Text: "123",
            Description: "Test address",
            Type: "Address",
            Highlight: "",
          },
        ],
      });
      const getSpy = vi.spyOn(addressLookupService, "get");

      render(
        <AddressLookup
          addressLookupService={addressLookupService}
          label="Find address"
          delay={0}
        />,
      );

      const input = screen.getByRole("combobox");

      // Type to trigger search and populate options
      await userEvent.type(input, "123");
      await screen.findByText("123, Test address");

      // Clear input and blur — Combobox calls onSelected with empty value
      await userEvent.clear(input);
      await act(async () => {
        await userEvent.tab();
      });

      expect(screen.queryByText("An error occurred")).not.toBeInTheDocument();
      expect(getSpy).not.toHaveBeenCalled();
    });

    it("should not call onError when the input is cleared and blurred", async () => {
      vi.spyOn(addressLookupService, "search").mockResolvedValue({
        Items: [
          {
            Id: "123",
            Text: "123",
            Description: "Test address",
            Type: "Address",
            Highlight: "",
          },
        ],
      });
      const onError = vi.fn();

      render(
        <AddressLookup
          addressLookupService={addressLookupService}
          label="Find address"
          delay={0}
          onError={onError}
        />,
      );

      const input = screen.getByRole("combobox");

      await userEvent.type(input, "123");
      await screen.findByText("123, Test address");

      await userEvent.clear(input);
      await act(async () => {
        await userEvent.tab();
      });

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

  it("should show sub-addresses after selecting a container type option, without error", async () => {
    vi.spyOn(addressLookupService, "search").mockResolvedValue(
      containerSearchResult,
    );
    vi.spyOn(addressLookupService, "findById").mockResolvedValue(
      subAddressResults,
    );

    render(
      <AddressLookup
        addressLookupService={addressLookupService}
        label="Find address"
        delay={0}
      />,
    );

    const input = screen.getByRole("combobox");
    await userEvent.type(input, "99G");

    const containerOption = await screen.findByText(
      "99 Gresham Street, London, EC2V 7NG - 3 Addresses",
    );
    await userEvent.click(containerOption);

    expect(
      screen.queryByText("Please select an option from the list"),
    ).not.toBeInTheDocument();
    expect(
      await screen.findByText("First Floor, 99 Gresham Street, London"),
    ).toBeInTheDocument();
    expect(
      screen.getByText("Second Floor, 99 Gresham Street, London"),
    ).toBeInTheDocument();
  });

  it("should call onOptionsDisplayed when valid results are displayed", async () => {
    vi.spyOn(addressLookupService, "search").mockResolvedValueOnce({
      Items: [
        {
          Id: "123",
          Text: "123",
          Description: "Test address",
          Type: "Address",
          Highlight: "",
        },
      ],
    });
    const mockOnOptionsDisplayed = vi.fn();
    render(
      <AddressLookup
        addressLookupService={addressLookupService}
        onOptionsDisplayed={mockOnOptionsDisplayed}
        label="Find address"
        delay={0}
      />,
    );

    const input = screen.getByRole("combobox");
    await userEvent.type(input, "123");

    const _ = await screen.findByText("123, Test address");
    expect(mockOnOptionsDisplayed).toHaveBeenCalled();
  });
});
