import type React from "react";

import type { IAsyncCheckGroup } from "@ax/components/Fields/AsyncCheckGroup";
import AsyncCheckGroup from "@ax/components/Fields/AsyncCheckGroup";
import { parseTheme } from "@ax/helpers";
import globalTheme from "@ax/themes/theme.json";
import type { ILanguage, ISite } from "@ax/types";

import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import axios from "axios";
import { mock } from "jest-mock-extended";
import { ThemeProvider } from "styled-components";

afterEach(cleanup);

jest.mock("axios");
const mockedAxios = axios as jest.MockedFunction<typeof axios>;

const buildOkResponse = (data: any[]) => ({
	data,
	status: 200,
	statusText: "Ok",
	headers: {},
	config: {},
});

const sampleOptions = [
	{ value: 3622, name: "Home", title: "Home" },
	{ value: 3731, name: "English child", title: "English child" },
	{ value: 3730, name: "English Parent", title: "English Parent" },
	{ value: 3787, name: "English test", title: "English test" },
	{ value: 3745, name: "New Page", title: "New Page" },
	{ value: 3784, name: "Prueba", title: "Prueba" },
];

const renderWithTheme = (props: IAsyncCheckGroup) =>
	render(
		<ThemeProvider theme={parseTheme(globalTheme)}>
			<AsyncCheckGroup {...props} />
		</ThemeProvider>,
	);

const rerenderWithTheme = (rerender: (ui: React.ReactElement) => void, props: IAsyncCheckGroup) =>
	rerender(
		<ThemeProvider theme={parseTheme(globalTheme)}>
			<AsyncCheckGroup {...props} />
		</ThemeProvider>,
	);

const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));

describe("AsyncCheckGroup component rendering", () => {
	afterEach(() => {
		jest.resetAllMocks();
	});

	it("should render the component with 6 options", async () => {
		const defaultSite = mock<ISite>();
		defaultSite.id = 113;

		const defaultDataProps: IAsyncCheckGroup = {
			site: defaultSite,
			value: [{ value: 3622, name: "Home", title: "Home" }],
			onChange: jest.fn(),
			source: "NEWS",
			contentType: "data",
		};

		mockedAxios.mockResolvedValue(buildOkResponse(sampleOptions));

		await act(async () => {
			renderWithTheme(defaultDataProps);
		});

		expect(screen.getAllByTestId("check-field-label")).toHaveLength(6);
	});

	it("should render the component with no options", async () => {
		const defaultProps: IAsyncCheckGroup = {
			value: [],
			onChange: jest.fn(),
			source: "NEWS",
			contentType: "data",
		};

		mockedAxios.mockResolvedValue({
			data: [],
			status: 500,
			statusText: "Internal Server Error",
			headers: {},
			config: {},
		});

		await act(async () => {
			renderWithTheme(defaultProps);
		});

		expect(screen.queryByTestId("check-field-label")).not.toBeTruthy();
	});
});

describe("AsyncCheckGroup - infinite render loop prevention", () => {
	afterEach(() => {
		jest.resetAllMocks();
	});

	it("should call the API only once across rerenders with unstable onChange and value references", async () => {
		const site = mock<ISite>();
		site.id = 1;

		mockedAxios.mockResolvedValue(buildOkResponse(sampleOptions));

		const baseProps = {
			site,
			source: "NEWS",
			contentType: "data" as const,
		};

		let rerender: (ui: React.ReactElement) => void = () => undefined;
		await act(async () => {
			({ rerender } = renderWithTheme({ ...baseProps, value: [], onChange: jest.fn() }));
		});

		for (let i = 0; i < 5; i++) {
			await act(async () => {
				rerenderWithTheme(rerender, { ...baseProps, value: [], onChange: jest.fn() });
				await flushPromises();
			});
		}

		expect(mockedAxios).toHaveBeenCalledTimes(1);
	});
});

describe("AsyncCheckGroup - refetch behavior on API-relevant prop changes", () => {
	afterEach(() => {
		jest.resetAllMocks();
	});

	it("should refetch when source changes", async () => {
		const site = mock<ISite>();
		site.id = 1;

		mockedAxios.mockResolvedValue(buildOkResponse(sampleOptions));

		const baseProps = {
			site,
			value: [],
			onChange: jest.fn(),
			contentType: "data" as const,
		};

		let rerender: (ui: React.ReactElement) => void = () => undefined;
		await act(async () => {
			({ rerender } = renderWithTheme({ ...baseProps, source: "NEWS" }));
		});

		const callsBefore = mockedAxios.mock.calls.length;

		await act(async () => {
			rerenderWithTheme(rerender, { ...baseProps, source: "EVENTS" });
			await flushPromises();
		});

		expect(mockedAxios.mock.calls.length).toBeGreaterThan(callsBefore);
	});

	it("should refetch when site changes", async () => {
		const siteA = mock<ISite>();
		siteA.id = 1;
		const siteB = mock<ISite>();
		siteB.id = 2;

		mockedAxios.mockResolvedValue(buildOkResponse(sampleOptions));

		const baseProps = {
			value: [],
			onChange: jest.fn(),
			source: "NEWS",
			contentType: "data" as const,
		};

		let rerender: (ui: React.ReactElement) => void = () => undefined;
		await act(async () => {
			({ rerender } = renderWithTheme({ ...baseProps, site: siteA }));
		});

		const callsBefore = mockedAxios.mock.calls.length;

		await act(async () => {
			rerenderWithTheme(rerender, { ...baseProps, site: siteB });
			await flushPromises();
		});

		expect(mockedAxios.mock.calls.length).toBeGreaterThan(callsBefore);
	});

	it("should refetch when contentLanguages changes", async () => {
		const site = mock<ISite>();
		site.id = 1;

		const languages: ILanguage[] = [{ id: 1, locale: "en_GB" } as ILanguage, { id: 2, locale: "es_ES" } as ILanguage];

		mockedAxios.mockResolvedValue(buildOkResponse(sampleOptions));

		const baseProps = {
			site,
			value: [],
			onChange: jest.fn(),
			source: "NEWS",
			contentType: "data" as const,
			languages,
		};

		let rerender: (ui: React.ReactElement) => void = () => undefined;
		await act(async () => {
			({ rerender } = renderWithTheme({ ...baseProps, contentLanguages: ["en_GB"] }));
		});

		const callsBefore = mockedAxios.mock.calls.length;

		await act(async () => {
			rerenderWithTheme(rerender, { ...baseProps, contentLanguages: ["es_ES"] });
			await flushPromises();
		});

		expect(mockedAxios.mock.calls.length).toBeGreaterThan(callsBefore);
	});

	it("should refetch when allLanguages changes", async () => {
		const site = mock<ISite>();
		site.id = 1;

		mockedAxios.mockResolvedValue(buildOkResponse(sampleOptions));

		const baseProps = {
			site,
			value: [],
			onChange: jest.fn(),
			source: "NEWS",
			contentType: "data" as const,
		};

		let rerender: (ui: React.ReactElement) => void = () => undefined;
		await act(async () => {
			({ rerender } = renderWithTheme({ ...baseProps, allLanguages: false }));
		});

		const callsBefore = mockedAxios.mock.calls.length;

		await act(async () => {
			rerenderWithTheme(rerender, { ...baseProps, allLanguages: true });
			await flushPromises();
		});

		expect(mockedAxios.mock.calls.length).toBeGreaterThan(callsBefore);
	});
});

describe("AsyncCheckGroup - user interaction", () => {
	afterEach(() => {
		jest.resetAllMocks();
	});

	it("should call onChange when a checkbox is toggled", async () => {
		const site = mock<ISite>();
		site.id = 1;

		const onChange = jest.fn();
		const props: IAsyncCheckGroup = {
			site,
			value: [],
			onChange,
			source: "NEWS",
			contentType: "data",
		};

		mockedAxios.mockResolvedValue(buildOkResponse(sampleOptions));

		await act(async () => {
			renderWithTheme(props);
		});

		const checkboxes = screen.getAllByTestId("check-field-label");
		await act(async () => {
			fireEvent.click(checkboxes[0]);
		});

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

	it("should always invoke the latest onChange even if the parent swaps it between renders", async () => {
		const site = mock<ISite>();
		site.id = 1;

		const firstOnChange = jest.fn();
		const secondOnChange = jest.fn();

		const baseProps = {
			site,
			value: [],
			source: "NEWS",
			contentType: "data" as const,
		};

		mockedAxios.mockResolvedValue(buildOkResponse(sampleOptions));

		let rerender: (ui: React.ReactElement) => void = () => undefined;
		await act(async () => {
			({ rerender } = renderWithTheme({ ...baseProps, onChange: firstOnChange }));
		});

		await act(async () => {
			rerenderWithTheme(rerender, { ...baseProps, onChange: secondOnChange });
			await flushPromises();
		});

		const checkboxes = screen.getAllByTestId("check-field-label");
		await act(async () => {
			fireEvent.click(checkboxes[0]);
		});

		expect(firstOnChange).not.toHaveBeenCalled();
		expect(secondOnChange).toHaveBeenCalled();
	});
});
