/**
 * @vitest-environment jsdom
 */
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Route, Router } from "wouter";
import { memoryLocation } from "wouter/memory-location";
import { apiClient } from "../api/client";
import { AuthProvider } from "../stores";
import { StudyPage } from "./StudyPage";

vi.mock("../api/client", () => ({
	apiClient: {
		login: vi.fn(),
		logout: vi.fn(),
		isAuthenticated: vi.fn(),
		getTokens: vi.fn(),
		getAuthHeader: vi.fn(),
		rpc: {
			api: {
				decks: {
					$get: vi.fn(),
					$post: vi.fn(),
				},
			},
		},
	},
	ApiClientError: class ApiClientError extends Error {
		constructor(
			message: string,
			public status: number,
			public code?: string,
		) {
			super(message);
			this.name = "ApiClientError";
		}
	},
}));

// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;

const mockDeck = {
	id: "deck-1",
	name: "Japanese Vocabulary",
	description: "Common Japanese words",
	newCardsPerDay: 20,
	createdAt: "2024-01-01T00:00:00Z",
	updatedAt: "2024-01-01T00:00:00Z",
};

const mockDueCards = [
	{
		id: "card-1",
		deckId: "deck-1",
		front: "Hello",
		back: "こんにちは",
		state: 0,
		due: "2024-01-01T00:00:00Z",
		stability: 0,
		difficulty: 0,
		elapsedDays: 0,
		scheduledDays: 0,
		reps: 0,
		lapses: 0,
		lastReview: null,
		createdAt: "2024-01-01T00:00:00Z",
		updatedAt: "2024-01-01T00:00:00Z",
		deletedAt: null,
		syncVersion: 0,
	},
	{
		id: "card-2",
		deckId: "deck-1",
		front: "Goodbye",
		back: "さようなら",
		state: 0,
		due: "2024-01-01T00:00:00Z",
		stability: 0,
		difficulty: 0,
		elapsedDays: 0,
		scheduledDays: 0,
		reps: 0,
		lapses: 0,
		lastReview: null,
		createdAt: "2024-01-01T00:00:00Z",
		updatedAt: "2024-01-01T00:00:00Z",
		deletedAt: null,
		syncVersion: 0,
	},
];

function renderWithProviders(path = "/decks/deck-1/study") {
	const { hook } = memoryLocation({ path, static: true });
	return render(
		<Router hook={hook}>
			<AuthProvider>
				<Route path="/decks/:deckId/study">
					<StudyPage />
				</Route>
			</AuthProvider>
		</Router>,
	);
}

describe("StudyPage", () => {
	beforeEach(() => {
		vi.clearAllMocks();
		vi.mocked(apiClient.getTokens).mockReturnValue({
			accessToken: "access-token",
			refreshToken: "refresh-token",
		});
		vi.mocked(apiClient.isAuthenticated).mockReturnValue(true);
		vi.mocked(apiClient.getAuthHeader).mockReturnValue({
			Authorization: "Bearer access-token",
		});
	});

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

	describe("Loading and Initial State", () => {
		it("shows loading state while fetching data", async () => {
			mockFetch.mockImplementation(() => new Promise(() => {})); // Never resolves

			renderWithProviders();

			// Loading state shows spinner (svg with animate-spin class)
			expect(document.querySelector(".animate-spin")).toBeDefined();
		});

		it("renders deck name and back link", async () => {
			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(
					screen.getByRole("heading", { name: /Japanese Vocabulary/ }),
				).toBeDefined();
			});

			expect(screen.getByText(/Back to Deck/)).toBeDefined();
		});

		it("passes auth header when fetching data", async () => {
			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: [] }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(mockFetch).toHaveBeenCalledWith("/api/decks/deck-1", {
					headers: { Authorization: "Bearer access-token" },
				});
			});
			expect(mockFetch).toHaveBeenCalledWith("/api/decks/deck-1/study", {
				headers: { Authorization: "Bearer access-token" },
			});
		});
	});

	describe("Error Handling", () => {
		it("displays error on API failure", async () => {
			mockFetch.mockResolvedValueOnce({
				ok: false,
				status: 404,
				json: async () => ({ error: "Deck not found" }),
			});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByRole("alert").textContent).toContain(
					"Deck not found",
				);
			});
		});

		it("allows retry after error", async () => {
			const user = userEvent.setup();
			// First call fails
			mockFetch
				.mockResolvedValueOnce({
					ok: false,
					status: 500,
					json: async () => ({ error: "Server error" }),
				})
				.mockResolvedValueOnce({
					ok: false,
					status: 500,
					json: async () => ({ error: "Server error" }),
				})
				// Retry succeeds
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByRole("alert")).toBeDefined();
			});

			await user.click(screen.getByRole("button", { name: "Retry" }));

			await waitFor(() => {
				expect(
					screen.getByRole("heading", { name: /Japanese Vocabulary/ }),
				).toBeDefined();
			});
		});
	});

	describe("No Cards State", () => {
		it("shows no cards message when deck has no due cards", async () => {
			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: [] }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("no-cards")).toBeDefined();
			});
			expect(screen.getByText("All caught up!")).toBeDefined();
			expect(
				screen.getByText("No cards due for review right now"),
			).toBeDefined();
		});
	});

	describe("Card Display and Progress", () => {
		it("shows remaining cards count", async () => {
			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("remaining-count").textContent).toBe(
					"2 remaining",
				);
			});
		});

		it("displays the front of the first card", async () => {
			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front").textContent).toBe("Hello");
			});
		});

		it("does not show rating buttons before card is flipped", async () => {
			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			expect(screen.queryByTestId("rating-buttons")).toBeNull();
		});
	});

	describe("Card Flip Interaction", () => {
		it("reveals answer when card is clicked", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			await user.click(screen.getByTestId("card-container"));

			expect(screen.getByTestId("card-back").textContent).toBe("こんにちは");
		});

		it("shows rating buttons after card is flipped", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			await user.click(screen.getByTestId("card-container"));

			expect(screen.getByTestId("rating-buttons")).toBeDefined();
			expect(screen.getByTestId("rating-1")).toBeDefined();
			expect(screen.getByTestId("rating-2")).toBeDefined();
			expect(screen.getByTestId("rating-3")).toBeDefined();
			expect(screen.getByTestId("rating-4")).toBeDefined();
		});

		it("displays rating labels on buttons", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			await user.click(screen.getByTestId("card-container"));

			expect(screen.getByTestId("rating-1").textContent).toContain("Again");
			expect(screen.getByTestId("rating-2").textContent).toContain("Hard");
			expect(screen.getByTestId("rating-3").textContent).toContain("Good");
			expect(screen.getByTestId("rating-4").textContent).toContain("Easy");
		});
	});

	describe("Rating Submission", () => {
		it("submits review and moves to next card", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				})
				// Submit review
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ card: { ...mockDueCards[0], reps: 1 } }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			// Flip card
			await user.click(screen.getByTestId("card-container"));

			// Rate as Good
			await user.click(screen.getByTestId("rating-3"));

			// Should move to next card
			await waitFor(() => {
				expect(screen.getByTestId("card-front").textContent).toBe("Goodbye");
			});

			// Verify API was called
			expect(mockFetch).toHaveBeenCalledWith(
				"/api/decks/deck-1/study/card-1",
				expect.objectContaining({
					method: "POST",
					headers: expect.objectContaining({
						Authorization: "Bearer access-token",
						"Content-Type": "application/json",
					}),
					body: expect.stringContaining('"rating":3'),
				}),
			);
		});

		it("updates remaining count after review", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ card: { ...mockDueCards[0], reps: 1 } }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("remaining-count").textContent).toBe(
					"2 remaining",
				);
			});

			await user.click(screen.getByTestId("card-container"));
			await user.click(screen.getByTestId("rating-3"));

			await waitFor(() => {
				expect(screen.getByTestId("remaining-count").textContent).toBe(
					"1 remaining",
				);
			});
		});

		it("shows error when rating submission fails", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				})
				.mockResolvedValueOnce({
					ok: false,
					status: 500,
					json: async () => ({ error: "Failed to submit review" }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			await user.click(screen.getByTestId("card-container"));
			await user.click(screen.getByTestId("rating-3"));

			await waitFor(() => {
				expect(screen.getByRole("alert").textContent).toContain(
					"Failed to submit review",
				);
			});
		});
	});

	describe("Session Complete", () => {
		it("shows session complete screen after all cards reviewed", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: [mockDueCards[0]] }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ card: { ...mockDueCards[0], reps: 1 } }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			// Review the only card
			await user.click(screen.getByTestId("card-container"));
			await user.click(screen.getByTestId("rating-3"));

			// Should show session complete
			await waitFor(() => {
				expect(screen.getByTestId("session-complete")).toBeDefined();
			});
			expect(screen.getByText("Session Complete!")).toBeDefined();
			expect(screen.getByTestId("completed-count").textContent).toBe("1");
		});

		it("shows correct count for multiple cards reviewed", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				})
				// First review
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ card: { ...mockDueCards[0], reps: 1 } }),
				})
				// Second review
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ card: { ...mockDueCards[1], reps: 1 } }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			// Review first card
			await user.click(screen.getByTestId("card-container"));
			await user.click(screen.getByTestId("rating-3"));

			// Review second card
			await waitFor(() => {
				expect(screen.getByTestId("card-front").textContent).toBe("Goodbye");
			});
			await user.click(screen.getByTestId("card-container"));
			await user.click(screen.getByTestId("rating-4"));

			// Should show session complete with 2 cards
			await waitFor(() => {
				expect(screen.getByTestId("session-complete")).toBeDefined();
			});
			expect(screen.getByTestId("completed-count").textContent).toBe("2");
		});

		it("provides navigation links after session complete", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: [mockDueCards[0]] }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ card: { ...mockDueCards[0], reps: 1 } }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			await user.click(screen.getByTestId("card-container"));
			await user.click(screen.getByTestId("rating-3"));

			await waitFor(() => {
				expect(screen.getByTestId("session-complete")).toBeDefined();
			});

			expect(screen.getAllByText("Back to Deck").length).toBeGreaterThan(0);
			expect(screen.getByText("All Decks")).toBeDefined();
		});
	});

	describe("Keyboard Shortcuts", () => {
		it("flips card with Space key", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			await user.keyboard(" ");

			expect(screen.getByTestId("card-back").textContent).toBe("こんにちは");
		});

		it("flips card with Enter key", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			await user.keyboard("{Enter}");

			expect(screen.getByTestId("card-back").textContent).toBe("こんにちは");
		});

		it("rates card with number keys", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ card: { ...mockDueCards[0], reps: 1 } }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			await user.keyboard(" "); // Flip
			await user.keyboard("3"); // Rate as Good

			await waitFor(() => {
				expect(screen.getByTestId("card-front").textContent).toBe("Goodbye");
			});

			expect(mockFetch).toHaveBeenCalledWith(
				"/api/decks/deck-1/study/card-1",
				expect.objectContaining({
					body: expect.stringContaining('"rating":3'),
				}),
			);
		});

		it("supports all rating keys (1, 2, 3, 4)", async () => {
			const user = userEvent.setup();

			mockFetch
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ deck: mockDeck }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ cards: mockDueCards }),
				})
				.mockResolvedValueOnce({
					ok: true,
					json: async () => ({ card: { ...mockDueCards[0], reps: 1 } }),
				});

			renderWithProviders();

			await waitFor(() => {
				expect(screen.getByTestId("card-front")).toBeDefined();
			});

			await user.keyboard(" "); // Flip
			await user.keyboard("1"); // Rate as Again

			expect(mockFetch).toHaveBeenCalledWith(
				"/api/decks/deck-1/study/card-1",
				expect.objectContaining({
					body: expect.stringContaining('"rating":1'),
				}),
			);
		});
	});
});
