import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SharedSchedule } from "../../components/SharedSchedule";

function renderSharedSchedule(path: string) {
	return render(
		<MemoryRouter initialEntries={[path]}>
			<Routes>
				<Route path="/share/:token" element={<SharedSchedule />} />
			</Routes>
		</MemoryRouter>
	);
}

function createSharedData() {
	return {
		sharedBy: "Owner",
		takenBy: "Max",
		scheduleDays: 30,
		medications: [],
	};
}

function createSharedDataWithEmbeddedOverview() {
	return {
		...createSharedData(),
		takenBy: "all",
		shareMedicationOverview: true,
		medicationOverview: [
			{
				name: "Aspirin",
				genericName: "Acetylsalicylic Acid",
				imageUrl: null,
				packageType: "blister",
				packCount: 1,
				packageAmountValue: null,
				packageAmountUnit: null,
				blistersPerPack: 2,
				pillsPerBlister: 10,
				totalPills: null,
				looseTablets: 0,
				currentStock: 8,
				capacity: 20,
				daysLeft: 8,
				nextIntakeDate: null,
				depletionDate: "2026-01-20",
				priority: "high",
				expiryDate: null,
				medicationStartDate: null,
				prescriptionEnabled: false,
				prescriptionRemainingRefills: null,
			},
			{
				name: "Vitamin D",
				genericName: null,
				imageUrl: null,
				packageType: "bottle",
				packCount: 0,
				packageAmountValue: null,
				packageAmountUnit: null,
				blistersPerPack: 1,
				pillsPerBlister: 1,
				totalPills: 100,
				looseTablets: 100,
				currentStock: 40,
				capacity: 100,
				daysLeft: 40,
				nextIntakeDate: null,
				depletionDate: "2026-02-21",
				priority: "normal",
				expiryDate: null,
				medicationStartDate: null,
				prescriptionEnabled: false,
				prescriptionRemainingRefills: null,
			},
			{
				name: "Hydrogel",
				genericName: null,
				imageUrl: null,
				packageType: "tube",
				packCount: 2,
				packageAmountValue: 40,
				packageAmountUnit: "g",
				blistersPerPack: 1,
				pillsPerBlister: 1,
				totalPills: 80,
				looseTablets: 80,
				currentStock: 80,
				capacity: 80,
				daysLeft: null,
				nextIntakeDate: null,
				depletionDate: null,
				priority: "normal",
				expiryDate: null,
				medicationStartDate: null,
				prescriptionEnabled: false,
				prescriptionRemainingRefills: null,
			},
			{
				name: "Cough Syrup",
				genericName: null,
				imageUrl: null,
				packageType: "liquid_container",
				packCount: 3,
				packageAmountValue: 150,
				packageAmountUnit: "ml",
				blistersPerPack: 1,
				pillsPerBlister: 1,
				totalPills: 450,
				looseTablets: 450,
				currentStock: 450,
				capacity: 450,
				daysLeft: null,
				nextIntakeDate: null,
				depletionDate: null,
				priority: "normal",
				expiryDate: null,
				medicationStartDate: null,
				prescriptionEnabled: false,
				prescriptionRemainingRefills: null,
			},
		],
	};
}

function createSharedDataWithTodayDose(referenceNow: Date) {
	const currentDay = new Date(referenceNow);
	currentDay.setHours(12, 0, 0, 0);
	const scheduledAt = new Date(currentDay);
	scheduledAt.setHours(9, 0, 0, 0);
	const dateOnlyMs = new Date(scheduledAt.getFullYear(), scheduledAt.getMonth(), scheduledAt.getDate()).getTime();
	const start = `${scheduledAt.getFullYear()}-${String(scheduledAt.getMonth() + 1).padStart(2, "0")}-${String(
		scheduledAt.getDate()
	).padStart(
		2,
		"0"
	)}T${String(scheduledAt.getHours()).padStart(2, "0")}:${String(scheduledAt.getMinutes()).padStart(2, "0")}:00`;

	return {
		sharedBy: "Owner",
		takenBy: "Max",
		scheduleDays: 30,
		allowJournalNotes: false,
		automaticDoseId: `1-0-${dateOnlyMs}`,
		medications: [
			{
				id: 1,
				name: "Ibuprofen",
				genericName: null,
				takenBy: [],
				packageType: "blister",
				packCount: 2,
				blistersPerPack: 1,
				pillsPerBlister: 10,
				looseTablets: 0,
				pillWeightMg: null,
				doseUnit: "mg",
				expiryDate: null,
				notes: null,
				intakeRemindersEnabled: false,
				blisters: [{ usage: 1, every: 1, start }],
				intakes: [{ usage: 1, every: 1, start, takenBy: null, intakeRemindersEnabled: false }],
				updatedAt: null,
				dismissedUntil: null,
				lastStockCorrectionAt: null,
			},
		],
	};
}

function createSharedDoseFetchMock(options: {
	token?: string;
	sharedData: ReturnType<typeof createSharedDataWithTodayDose>;
	initialDoses?: Array<{
		doseId: string;
		skipped?: boolean;
		dismissed?: boolean;
		takenSource?: string;
		hasJournalNote?: boolean;
	}>;
}) {
	const token = options.token ?? "token-123";
	const doseState = new Map((options.initialDoses ?? []).map((dose) => [dose.doseId, { ...dose }]));
	const journalState = new Map<string, { note: string | null; createdAt: string | null; updatedAt: string | null }>();
	const requests: Array<{ url: string; method: string; body?: unknown }> = [];

	const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
		const method = init?.method ?? "GET";
		const body =
			typeof init?.body === "string" && init.body.length > 0
				? (JSON.parse(init.body) as { doseId?: string; note?: string | null })
				: undefined;
		requests.push({ url, method, body });

		if (url === `/api/share/${token}` && method === "GET") {
			return { ok: true, json: async () => options.sharedData };
		}

		if (url === `/api/share/${token}/doses` && method === "GET") {
			const doses = Array.from(doseState.values()).map((dose) => ({
				...dose,
				hasJournalNote: dose.hasJournalNote === true || Boolean(journalState.get(dose.doseId)?.note?.trim()),
			}));
			return { ok: true, json: async () => ({ doses }) };
		}

		if (url === `/api/share/${token}/doses/skip` && method === "POST" && body?.doseId) {
			doseState.set(body.doseId, { doseId: body.doseId, skipped: true });
			return { ok: true, json: async () => ({}) };
		}

		if (url === `/api/share/${token}/doses` && method === "POST" && body?.doseId) {
			doseState.set(body.doseId, { doseId: body.doseId, takenSource: "manual" });
			return { ok: true, json: async () => ({}) };
		}

		if (url.startsWith(`/api/share/${token}/journal/event/`) && method === "GET") {
			const doseId = decodeURIComponent(url.split("/").at(-1) ?? "");
			const journal = journalState.get(doseId) ?? { note: null, createdAt: null, updatedAt: null };
			return {
				ok: true,
				json: async () => ({
					entry: {
						doseTrackingId: 1,
						doseId,
						medicationId: 1,
						medicationName: "Ibuprofen",
						scheduledFor: new Date().toISOString(),
						takenAt: new Date().toISOString(),
						dismissed: false,
						takenSource: "manual",
						markedBy: "Max",
						note: journal.note,
						createdAt: journal.createdAt,
						updatedAt: journal.updatedAt,
					},
				}),
			};
		}

		if (url.startsWith(`/api/share/${token}/journal/event/`) && method === "PUT") {
			const doseId = decodeURIComponent(url.split("/").at(-1) ?? "");
			const timestamp = new Date().toISOString();
			journalState.set(doseId, { note: body?.note ?? null, createdAt: timestamp, updatedAt: timestamp });
			return {
				ok: true,
				json: async () => ({
					entry: {
						doseTrackingId: 1,
						doseId,
						medicationId: 1,
						medicationName: "Ibuprofen",
						scheduledFor: new Date().toISOString(),
						takenAt: new Date().toISOString(),
						dismissed: false,
						takenSource: "manual",
						markedBy: "Max",
						note: body?.note ?? null,
						createdAt: timestamp,
						updatedAt: timestamp,
					},
				}),
			};
		}

		if (url.startsWith(`/api/share/${token}/journal/event/`) && method === "DELETE") {
			const doseId = decodeURIComponent(url.split("/").at(-1) ?? "");
			journalState.delete(doseId);
			return { ok: true, json: async () => ({ success: true }) };
		}

		if (url.startsWith(`/api/share/${token}/doses/skip/`) && method === "DELETE") {
			const doseId = decodeURIComponent(url.split("/").at(-1) ?? "");
			doseState.delete(doseId);
			return { ok: true, json: async () => ({}) };
		}

		return Promise.reject(new Error(`Unexpected request: ${method} ${url}`));
	});

	return { fetchMock, requests, getDoses: () => Array.from(doseState.values()) };
}

describe("SharedSchedule", () => {
	beforeEach(() => {
		vi.clearAllMocks();
		window.localStorage.clear();
		globalThis.fetch = vi.fn() as unknown as typeof fetch;
		vi.spyOn(globalThis, "setInterval").mockImplementation(() => 1 as unknown as ReturnType<typeof setInterval>);
		vi.spyOn(globalThis, "clearInterval").mockImplementation(() => {});
	});

	afterEach(() => {
		cleanup();
		vi.restoreAllMocks();
	});

	it("renders shared schedule shell for valid token", async () => {
		(globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation((url: string, init?: RequestInit) => {
			if (url === "/api/share/token-123/doses" && (!init?.method || init.method === "GET")) {
				return Promise.resolve({ ok: true, json: () => Promise.resolve({ doses: [] }) });
			}
			if (url === "/api/share/token-123") {
				return Promise.resolve({ ok: true, json: () => Promise.resolve(createSharedData()) });
			}
			return Promise.reject(new Error(`Unexpected URL: ${url}`));
		});

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(screen.getByText(/share\.scheduleFor/i)).toBeInTheDocument();
			expect(screen.getByText("share.publicAccessHelp")).toBeInTheDocument();
			expect(screen.getByText("share.noSchedule")).toBeInTheDocument();
		});
	});

	it("opens and saves a shared journal note when the share link allows notes", async () => {
		const referenceNow = new Date();
		referenceNow.setHours(12, 0, 0, 0);
		vi.spyOn(Date, "now").mockReturnValue(referenceNow.getTime());
		const sharedData = {
			...createSharedDataWithTodayDose(referenceNow),
			allowJournalNotes: true,
		};
		const { fetchMock, requests } = createSharedDoseFetchMock({
			sharedData,
		});
		globalThis.fetch = fetchMock as unknown as typeof fetch;

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(document.querySelector(".dose-btn.take")).toBeInTheDocument();
		});

		const unavailableJournalButton = document.querySelector(".dose-btn.journal") as HTMLButtonElement;
		expect(unavailableJournalButton).toBeDisabled();
		expect(unavailableJournalButton).not.toHaveClass("has-note");
		expect(unavailableJournalButton.closest("span")).toHaveAttribute("data-tooltip", "journal.actions.noteTakenOnly");

		fireEvent.click(screen.getByText("dose.take"));

		await waitFor(() => {
			expect(requests).toContainEqual({
				url: "/api/share/token-123/doses",
				method: "POST",
				body: { doseId: sharedData.automaticDoseId },
			});
			expect(document.querySelector(".day-block.today")).not.toHaveClass("collapsed");
		});

		await waitFor(() => {
			const availableJournalButton = document.querySelector(".dose-btn.journal") as HTMLButtonElement;
			expect(availableJournalButton).not.toBeDisabled();
			expect(availableJournalButton).not.toHaveClass("has-note");
			expect(availableJournalButton.closest("span")).not.toHaveAttribute("data-tooltip");
		});

		fireEvent.click(document.querySelector(".dose-btn.journal") as Element);

		await waitFor(() => {
			expect(requests).toContainEqual({
				url: `/api/share/token-123/journal/event/${sharedData.automaticDoseId}`,
				method: "GET",
				body: undefined,
			});
		});

		await waitFor(() => {
			expect(screen.getByLabelText("journal.editor.noteLabel")).toHaveValue("");
		});
		expect(screen.queryByRole("button", { name: "common.delete" })).not.toBeInTheDocument();

		fireEvent.change(screen.getByLabelText("journal.editor.noteLabel"), { target: { value: "Shared note" } });
		fireEvent.click(screen.getByRole("button", { name: "common.save" }));

		await waitFor(() => {
			expect(requests).toContainEqual({
				url: `/api/share/token-123/journal/event/${sharedData.automaticDoseId}`,
				method: "PUT",
				body: { note: "Shared note" },
			});
		});

		await waitFor(() => {
			expect(screen.queryByLabelText("journal.editor.noteLabel")).not.toBeInTheDocument();
			const savedJournalButton = document.querySelector(".dose-btn.journal") as HTMLButtonElement;
			expect(savedJournalButton).toHaveClass("has-note");
		});
	});

	it("marks shared journal notes from the shared dose read state", async () => {
		const referenceNow = new Date();
		referenceNow.setHours(12, 0, 0, 0);
		vi.spyOn(Date, "now").mockReturnValue(referenceNow.getTime());
		const sharedData = {
			...createSharedDataWithTodayDose(referenceNow),
			allowJournalNotes: true,
		};
		const { fetchMock } = createSharedDoseFetchMock({
			sharedData,
			initialDoses: [{ doseId: sharedData.automaticDoseId, takenSource: "manual", hasJournalNote: true }],
		});
		globalThis.fetch = fetchMock as unknown as typeof fetch;

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			const journalButton = document.querySelector(".dose-btn.journal") as HTMLButtonElement;
			expect(journalButton).not.toBeDisabled();
			expect(journalButton).toHaveClass("has-note");
		});
	});

	it("renders not found state for missing share link", async () => {
		(globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
			if (url === "/api/share/token-123/doses") {
				return Promise.resolve({ ok: true, json: () => Promise.resolve({ doses: [] }) });
			}
			if (url === "/api/share/token-123") {
				return Promise.resolve({ ok: false, status: 404, json: () => Promise.resolve({}) });
			}
			return Promise.reject(new Error(`Unexpected URL: ${url}`));
		});

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(screen.getByText("share.notFound")).toBeInTheDocument();
		});
	});

	it("renders expired state for expired share links", async () => {
		(globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
			if (url === "/api/share/token-123/doses") {
				return Promise.resolve({ ok: true, json: () => Promise.resolve({ doses: [] }) });
			}
			if (url === "/api/share/token-123") {
				return Promise.resolve({
					ok: false,
					status: 410,
					json: () =>
						Promise.resolve({
							ownerUsername: "owner",
							takenBy: "Max",
							expiredAt: "2026-02-01T10:00:00.000Z",
						}),
				});
			}
			return Promise.reject(new Error(`Unexpected URL: ${url}`));
		});

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(screen.getByText("share.expired.title")).toBeInTheDocument();
		});
	});

	it("renders generic error when loading share data fails", async () => {
		(globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation((url: string, init?: RequestInit) => {
			if (url === "/api/share/token-123/doses" && (!init?.method || init.method === "GET")) {
				return Promise.resolve({ ok: true, json: () => Promise.resolve({ doses: [] }) });
			}
			if (url === "/api/share/token-123") {
				return Promise.reject(new Error("network failed"));
			}
			return Promise.reject(new Error(`Unexpected URL: ${url}`));
		});

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(screen.getByText("share.error")).toBeInTheDocument();
		});
	});

	it("shows the robot marker for automatically taken shared doses", async () => {
		const referenceNow = new Date();
		referenceNow.setHours(12, 0, 0, 0);
		vi.spyOn(Date, "now").mockReturnValue(referenceNow.getTime());
		const sharedData = createSharedDataWithTodayDose(referenceNow);

		(globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation((url: string, init?: RequestInit) => {
			if (url === "/api/share/token-123/doses" && (!init?.method || init.method === "GET")) {
				return Promise.resolve({
					ok: true,
					json: () =>
						Promise.resolve({
							doses: [{ doseId: sharedData.automaticDoseId, dismissed: false, takenSource: "automatic" }],
						}),
				});
			}
			if (url === "/api/share/token-123") {
				return Promise.resolve({ ok: true, json: () => Promise.resolve(sharedData) });
			}
			return Promise.reject(new Error(`Unexpected URL: ${url}`));
		});

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(screen.getByText("🤖")).toBeInTheDocument();
		});
	});

	it("renders the embedded medication overview on the shared page when enabled", async () => {
		const sharedData = createSharedDataWithEmbeddedOverview();

		(globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation((url: string, init?: RequestInit) => {
			if (url === "/api/share/token-123/doses" && (!init?.method || init.method === "GET")) {
				return Promise.resolve({ ok: true, json: () => Promise.resolve({ doses: [] }) });
			}
			if (url === "/api/share/token-123") {
				return Promise.resolve({ ok: true, json: () => Promise.resolve(sharedData) });
			}
			return Promise.reject(new Error(`Unexpected URL: ${url}`));
		});

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(screen.getAllByText("Aspirin").length).toBeGreaterThan(0);
			expect(screen.getAllByText("Acetylsalicylic Acid").length).toBeGreaterThan(0);
		});

		expect(screen.getByText("sharedOverview.columns.priority")).toBeInTheDocument();
		expect(screen.getAllByText("100").length).toBeGreaterThan(0);
		expect(screen.getAllByText("2 x 40 form.packageAmountUnitG").length).toBeGreaterThan(0);
		expect(screen.getAllByText("3 x 150 form.packageAmountUnitMl").length).toBeGreaterThan(0);
		expect(screen.getByText("share.noSchedule")).toBeInTheDocument();
	});

	it("skips a neutral shared dose via the skip endpoint", async () => {
		const referenceNow = new Date();
		referenceNow.setHours(12, 0, 0, 0);
		vi.spyOn(Date, "now").mockReturnValue(referenceNow.getTime());
		const sharedData = createSharedDataWithTodayDose(referenceNow);
		const { fetchMock, requests } = createSharedDoseFetchMock({ sharedData });
		globalThis.fetch = fetchMock as unknown as typeof fetch;

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(document.querySelector(".dose-btn.skip")).toBeInTheDocument();
		});

		fireEvent.click(screen.getByText("dose.skip"));

		await waitFor(() => {
			expect(requests).toContainEqual({
				url: "/api/share/token-123/doses/skip",
				method: "POST",
				body: { doseId: sharedData.automaticDoseId },
			});
			expect(document.querySelector(".dose-btn.undo.skip")).toBeInTheDocument();
		});
	});

	it("undoes a skipped shared dose via the delete skip endpoint", async () => {
		const referenceNow = new Date();
		referenceNow.setHours(12, 0, 0, 0);
		vi.spyOn(Date, "now").mockReturnValue(referenceNow.getTime());
		const sharedData = createSharedDataWithTodayDose(referenceNow);
		const { fetchMock, requests } = createSharedDoseFetchMock({
			sharedData,
			initialDoses: [{ doseId: sharedData.automaticDoseId, skipped: true }],
		});
		globalThis.fetch = fetchMock as unknown as typeof fetch;

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(document.querySelector(".dose-btn.undo.skip")).toBeInTheDocument();
		});

		fireEvent.click(screen.getByText("dose.undoSkip"));

		await waitFor(() => {
			expect(requests).toContainEqual({
				url: `/api/share/token-123/doses/skip/${sharedData.automaticDoseId}`,
				method: "DELETE",
			});
			expect(document.querySelector(".dose-btn.skip")).toBeInTheDocument();
		});
	});

	it("takes a skipped shared dose again via the take endpoint", async () => {
		const referenceNow = new Date();
		referenceNow.setHours(12, 0, 0, 0);
		vi.spyOn(Date, "now").mockReturnValue(referenceNow.getTime());
		const sharedData = createSharedDataWithTodayDose(referenceNow);
		const { fetchMock, requests, getDoses } = createSharedDoseFetchMock({
			sharedData,
			initialDoses: [{ doseId: sharedData.automaticDoseId, skipped: true }],
		});
		globalThis.fetch = fetchMock as unknown as typeof fetch;

		renderSharedSchedule("/share/token-123");

		await waitFor(() => {
			expect(document.querySelector(".dose-btn.undo.skip")).toBeInTheDocument();
		});

		fireEvent.click(screen.getByText("dose.take"));

		await waitFor(() => {
			expect(requests).toContainEqual({
				url: "/api/share/token-123/doses",
				method: "POST",
				body: { doseId: sharedData.automaticDoseId },
			});
			expect(getDoses()).toEqual([
				expect.objectContaining({ doseId: sharedData.automaticDoseId, takenSource: "manual" }),
			]);
			expect(document.querySelector(".day-block.today")).toHaveClass("all-taken");
		});
	});
});
