import { test, describe, assert, afterEach, vi } from "vitest";
import { cleanup, render } from "@gradio/tootils";
import event from "@testing-library/user-event";
import { setupi18n } from "../app/src/i18n";

import Dropdown from "./Index.svelte";
import type { LoadingStatus } from "@gradio/statustracker";

const loading_status: LoadingStatus = {
	eta: 0,
	queue_position: 1,
	queue_size: 1,
	status: "complete" as LoadingStatus["status"],
	scroll_to_output: false,
	visible: true,
	fn_index: 0,
	show_progress: "full"
};

describe("Dropdown", () => {
	afterEach(() => {
		cleanup();
		vi.useRealTimers();
	});
	beforeEach(() => {
		setupi18n();
	});
	test("renders provided value", async () => {
		const { getByLabelText } = await render(Dropdown, {
			show_label: true,
			loading_status,
			max_choices: null,
			value: "choice",
			label: "Dropdown",
			choices: [
				["choice", "choice"],
				["choice2", "choice2"]
			],
			filterable: false,
			interactive: false
		});

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;
		assert.equal(item.value, "choice");
	});

	test("selecting the textbox should show the options", async () => {
		const { getByLabelText, getAllByTestId } = await render(Dropdown, {
			show_label: true,
			loading_status,
			max_choices: 10,
			value: "choice",
			label: "Dropdown",
			choices: [
				["choice", "choice"],
				["name2", "choice2"]
			],
			filterable: true,
			interactive: true
		});

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		await item.focus();

		const options = getAllByTestId("dropdown-option");

		expect(options).toHaveLength(2);
		expect(options[0]).toContainHTML("choice");
		expect(options[1]).toContainHTML("name2");
	});

	test("editing the textbox value should trigger the type event and filter the options", async () => {
		const { getByLabelText, listen, getAllByTestId } = await render(Dropdown, {
			show_label: true,
			loading_status,
			max_choices: 10,
			value: "",
			label: "Dropdown",
			choices: [
				["apple", "apple"],
				["zebra", "zebra"]
			],
			filterable: true,
			interactive: true
		});

		const key_up_event = listen("key_up");

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		await item.focus();
		const options = getAllByTestId("dropdown-option");

		expect(options).toHaveLength(2);

		item.value = "";
		await event.keyboard("z");

		const options_new = getAllByTestId("dropdown-option");

		await expect(options_new).toHaveLength(1);
		await expect(options[0]).toContainHTML("zebra");
		await assert.equal(key_up_event.callCount, 1);
	});

	test("blurring the textbox should cancel the filter", async () => {
		const { getByLabelText, listen } = await render(Dropdown, {
			show_label: true,
			loading_status,
			value: "default",
			label: "Dropdown",
			max_choices: undefined,
			choices: [
				["default", "default"],
				["other", "other"]
			],
			filterable: false,
			interactive: true
		});

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		item.focus();
		await event.keyboard("other");
	});

	test("blurring the textbox should save the input value", async () => {
		const { getByLabelText, listen } = await render(Dropdown, {
			show_label: true,
			loading_status,
			value: "new ",
			label: "Dropdown",
			max_choices: undefined,
			allow_custom_value: true,
			choices: [
				["dwight", "dwight"],
				["michael", "michael"]
			],
			filterable: true,
			interactive: true
		});

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;
		const change_event = listen("change");

		item.focus();
		await event.keyboard("kevin");
		await item.blur();

		assert.equal(item.value, "new kevin");
		assert.equal(change_event.callCount, 1);
	});

	test("focusing the label should toggle the options", async () => {
		const { getByLabelText, listen } = await render(Dropdown, {
			show_label: true,
			loading_status,
			value: "default",
			label: "Dropdown",
			choices: [
				["default", "default"],
				["other", "other"]
			],
			filterable: true,
			interactive: true
		});

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;
		const blur_event = listen("blur");
		const focus_event = listen("focus");

		item.focus();
		item.blur();

		assert.equal(blur_event.callCount, 1);
		assert.equal(focus_event.callCount, 1);
	});

	test("deselecting and reselcting a filtered dropdown should show all options again", async () => {
		vi.useFakeTimers();
		const { getByLabelText, getAllByTestId } = await render(Dropdown, {
			show_label: true,
			loading_status,
			max_choices: 10,
			value: "",
			label: "Dropdown",
			choices: [
				["apple", "apple"],
				["zebra", "zebra"],
				["pony", "pony"]
			],
			filterable: true,
			interactive: true
		});

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		item.focus();
		item.value = "";
		await event.keyboard("z");
		const options = getAllByTestId("dropdown-option");

		expect(options).toHaveLength(1);

		await item.blur();
		// Mock 100ms delay between interactions.
		vi.runAllTimers();
		await item.focus();
		const options_new = getAllByTestId("dropdown-option");

		expect(options_new).toHaveLength(3);
	});

	test("passing in a new set of identical choices when the dropdown is open should not filter the dropdown", async () => {
		const { getByLabelText, getAllByTestId, component } = await render(
			Dropdown,
			{
				show_label: true,
				loading_status,
				value: "",
				label: "Dropdown",
				choices: [
					["apple", "apple"],
					["zebra", "zebra"],
					["pony", "pony"]
				],
				filterable: true,
				interactive: true
			}
		);

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		await item.focus();

		const options = getAllByTestId("dropdown-option");

		expect(options).toHaveLength(3);

		component.$set({
			value: "",
			choices: [
				["apple", "apple"],
				["zebra", "zebra"],
				["pony", "pony"]
			]
		});

		item.focus();

		const options_new = getAllByTestId("dropdown-option");
		expect(options_new).toHaveLength(3);
	});

	test("setting a custom value when allow_custom_choice is false should revert to the first valid choice", async () => {
		const { getByLabelText, getAllByTestId, component } = await render(
			Dropdown,
			{
				show_label: true,
				loading_status,
				value: "",
				allow_custom_value: false,
				label: "Dropdown",
				choices: [
					["apple", "apple"],
					["zebra", "zebra"],
					["pony", "pony"]
				],
				filterable: true,
				interactive: true
			}
		);

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		await item.focus();
		await event.keyboard("pie");
		expect(item.value).toBe("applepie");
		await item.blur();
		expect(item.value).toBe("apple");
	});

	test("setting a custom value when allow_custom_choice is true should keep the value", async () => {
		const { getByLabelText, getAllByTestId, component } = await render(
			Dropdown,
			{
				show_label: true,
				loading_status,
				value: "",
				allow_custom_value: true,
				label: "Dropdown",
				choices: [
					["apple", "apple"],
					["zebra", "zebra"],
					["pony", "pony"]
				],
				filterable: true,
				interactive: true
			}
		);

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		await item.focus();
		await event.keyboard("pie");
		expect(item.value).toBe("applepie");
		await item.blur();
		expect(item.value).toBe("applepie");
	});

	test("setting a value should update the displayed value and selected indices", async () => {
		const { getByLabelText, getAllByTestId, component } = await render(
			Dropdown,
			{
				show_label: true,
				loading_status,
				value: "",
				allow_custom_value: false,
				label: "Dropdown",
				choices: [
					["apple", "apple"],
					["zebra", "zebra"],
					["pony", "pony"]
				],
				filterable: true,
				interactive: true
			}
		);

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		expect(item.value).toBe("apple");
		await item.focus();
		let options = getAllByTestId("dropdown-option");
		expect(options[0]).toHaveClass("selected");

		await component.$set({ value: "zebra" });
		expect(item.value).toBe("zebra");
		options = getAllByTestId("dropdown-option");
		expect(options[0]).toHaveClass("selected");

		await component.$set({ value: undefined });
		expect(item.value).toBe("");
		options = getAllByTestId("dropdown-option");
		expect(options[0]).not.toHaveClass("selected");

		await component.$set({ value: "zebra" });
		expect(item.value).toBe("zebra");
		options = getAllByTestId("dropdown-option");
		expect(options[0]).toHaveClass("selected");
	});

	test("blurring a dropdown should set the input text to the previously selected value", async () => {
		const { getByLabelText, getAllByTestId, component } = await render(
			Dropdown,
			{
				show_label: true,
				loading_status,
				value: "",
				allow_custom_value: false,
				label: "Dropdown",
				choices: [
					["apple", "apple_internal_value"],
					["zebra", "zebra_internal_value"],
					["pony", "pony_internal_value"]
				],
				filterable: true,
				interactive: true
			}
		);

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		expect(item.value).toBe("apple");
		await item.focus();
		let options = getAllByTestId("dropdown-option");
		expect(options[0]).toHaveClass("selected");
		await item.blur();
		expect(item.value).toBe("apple");

		await item.focus();
		await event.keyboard("z");
		expect(item.value).toBe("applez");
		await item.blur();
		expect(item.value).toBe("apple");
	});

	test("updating choices should keep the dropdown focus-able and change the value appropriately if custom values are not allowed", async () => {
		const { getByLabelText, component } = await render(Dropdown, {
			show_label: true,
			loading_status,
			value: "apple_internal_value",
			allow_custom_value: false,
			label: "Dropdown",
			choices: [
				["apple_choice", "apple_internal_value"],
				["zebra_choice", "zebra_internal_value"]
			],
			filterable: true,
			interactive: true
		});

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		await expect(item.value).toBe("apple_choice");

		component.$set({
			choices: [
				["apple_new_choice", "apple_internal_value"],
				["zebra_new_choice", "zebra_internal_value"]
			]
		});

		await item.focus();
		await item.blur();
		await expect(item.value).toBe("apple_new_choice");
	});

	test("updating choices should not reset the value if custom values are allowed", async () => {
		const { getByLabelText, component } = await render(Dropdown, {
			show_label: true,
			loading_status,
			value: "apple_internal_value",
			allow_custom_value: true,
			label: "Dropdown",
			choices: [
				["apple_choice", "apple_internal_value"],
				["zebra_choice", "zebra_internal_value"]
			],
			filterable: true,
			interactive: true
		});

		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;

		await expect(item.value).toBe("apple_choice");

		component.$set({
			choices: [
				["apple_new_choice", "apple_internal_value"],
				["zebra_new_choice", "zebra_internal_value"]
			]
		});

		await expect(item.value).toBe("apple_choice");
	});

	test("ensure dropdown can have an empty value", async () => {
		const { getByLabelText } = await render(Dropdown, {
			show_label: true,
			loading_status,
			allow_custom_value: false,
			label: "Dropdown",
			choices: [
				["apple_choice", "apple_internal_value"],
				["zebra_choice", "zebra_internal_value"]
			],
			filterable: true,
			interactive: true
		});
		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;
		await expect(item.value).toBe("");
	});

	test("ensure dropdown works when initial value is undefined and allow custom value is set", async () => {
		const { getByLabelText } = await render(Dropdown, {
			show_label: true,
			loading_status,
			value: undefined,
			allow_custom_value: true,
			label: "Dropdown",
			choices: [
				["apple_choice", "apple_internal_value"],
				["zebra_choice", "zebra_internal_value"]
			],
			filterable: true,
			interactive: true
		});
		const item: HTMLInputElement = getByLabelText(
			"Dropdown"
		) as HTMLInputElement;
		await expect(item.value).toBe("");
	});
});
