import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, waitFor, act } from "@testing-library/react";
import { useEvents } from "./use-events";

// Mock the API client
vi.mock("../api/events", () => ({
	createEventSource: vi.fn(),
	probeEventStreamStatus: vi.fn().mockResolvedValue(null),
}));

import { createEventSource, probeEventStreamStatus } from "../api/events";

const mockedCreateEventSource = vi.mocked(createEventSource);
const mockedProbeEventStreamStatus = vi.mocked(probeEventStreamStatus);

describe("useEvents", () => {
	let mockEs: EventSource & {
		emit: (type: string, data: string) => void;
	};

	beforeEach(() => {
		vi.useFakeTimers({ shouldAdvanceTime: true });
		const listeners = new Map<string, Set<(e: MessageEvent) => void>>();
		mockEs = {
			close: vi.fn(),
			onopen: null,
			onmessage: null,
			onerror: null,
			get readyState() {
				return EventSource.OPEN;
			},
			url: "http://localhost:8000/events/stream",
			addEventListener: vi.fn(
				(type: string, handler: (e: MessageEvent) => void) => {
					if (!listeners.has(type)) listeners.set(type, new Set());
					listeners.get(type)!.add(handler);
				},
			),
			removeEventListener: vi.fn(
				(type: string, handler: (e: MessageEvent) => void) => {
					listeners.get(type)?.delete(handler);
				},
			),
			emit: (type: string, data: string) => {
				listeners
					.get(type)
					?.forEach((handler) => handler({ data } as MessageEvent));
			},
		} as unknown as EventSource & {
			emit: (type: string, data: string) => void;
		};
		mockedCreateEventSource.mockReturnValue(mockEs);
		mockedProbeEventStreamStatus.mockResolvedValue(null);
	});

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

	it("parses sse event and adds to state", async () => {
		const { result } = renderHook(() => useEvents());

		// Simulate connection open
		act(() => {
			mockEs.onopen?.({} as Event);
		});

		const payload = {
			event: "instance.started",
			instance_id: "abc-123",
			status: "starting",
			message: "Container starting...",
			metadata: {},
			timestamp: "2026-05-28T12:00:00Z",
			correlation_id: "corr-1",
		};

		act(() => {
			mockEs.emit("instance.started", JSON.stringify(payload));
		});

		await waitFor(() => {
			expect(result.current.events).toHaveLength(1);
			expect(result.current.events[0].instance_id).toBe("abc-123");
		});
		expect(result.current.connected).toBe(true);
	});

	it("reconnects with exponential backoff on error", async () => {
		renderHook(() => useEvents());

		act(() => {
			mockEs.onerror?.({} as Event);
		});

		expect(mockEs.close).toHaveBeenCalled();
		expect(mockedCreateEventSource).toHaveBeenCalledTimes(1);

		// Advance past first backoff (should be ~1000ms)
		await act(async () => {
			vi.advanceTimersByTime(1200);
		});

		expect(mockedCreateEventSource).toHaveBeenCalledTimes(2);

		// Trigger another error
		const secondEs = mockedCreateEventSource.mock.results[1]
			.value as EventSource;
		act(() => {
			secondEs.onerror?.({} as Event);
		});

		// Advance past second backoff (should be ~2000ms)
		await act(async () => {
			vi.advanceTimersByTime(2500);
		});

		expect(mockedCreateEventSource).toHaveBeenCalledTimes(3);
	});

	it("caps reconnect delay at 30 seconds", async () => {
		renderHook(() => useEvents());

		// Trigger 6 errors to get past 1s, 2s, 4s, 8s, 16s
		for (let i = 0; i < 6; i++) {
			const currentEs =
				i === 0
					? mockEs
					: (mockedCreateEventSource.mock.results[i]?.value as EventSource);
			act(() => {
				currentEs.onerror?.({} as Event);
			});

			// Advance enough to trigger next reconnect
			await act(async () => {
				vi.advanceTimersByTime(35000);
			});
		}

		expect(mockedCreateEventSource.mock.calls.length).toBeGreaterThan(5);
	});

	it("stops reconnecting and redirects on 401", async () => {
		mockedProbeEventStreamStatus.mockResolvedValue(401);
		const originalLocation = window.location;
		// @ts-expect-error - overriding readonly location for test
		delete window.location;
		// @ts-expect-error - mock location
		window.location = { ...originalLocation, assign: vi.fn() };

		renderHook(() => useEvents());

		act(() => {
			mockEs.onerror?.({} as Event);
		});

		await act(async () => {
			vi.advanceTimersByTime(1500);
		});

		expect(window.location.assign).toHaveBeenCalled();
		// @ts-expect-error - restoring location
		window.location = originalLocation;
	});

	it("adds 5s penalty on 429", async () => {
		mockedProbeEventStreamStatus.mockResolvedValue(429);

		renderHook(() => useEvents());

		act(() => {
			mockEs.onerror?.({} as Event);
		});

		// First timeout fires (~1s), detects 429, schedules penalty timeout (~7s later)
		await act(async () => {
			vi.advanceTimersByTime(2000);
		});

		expect(mockedCreateEventSource).toHaveBeenCalledTimes(1);

		// Advance past penalty delay (need enough for delay + 5000)
		await act(async () => {
			vi.advanceTimersByTime(10000);
		});

		expect(mockedCreateEventSource).toHaveBeenCalledTimes(2);
	});

	it("cleans up EventSource on unmount", () => {
		const { unmount } = renderHook(() => useEvents());
		unmount();
		expect(mockEs.close).toHaveBeenCalled();
	});

	it("exposes reconnectCount", async () => {
		const { result } = renderHook(() => useEvents());

		act(() => {
			mockEs.onerror?.({} as Event);
		});

		await waitFor(() => {
			expect(result.current.reconnectCount).toBeGreaterThan(0);
		});
	});
});
