import * as fs from "node:fs";
import * as path from "node:path";
import { type APIResponse, expect, type Page, test as setup } from "@playwright/test";
import { applyVideoSafetyMode, TEST_USER } from "./fixtures";

const authFile = path.join(import.meta.dirname, ".auth", "user.json");

type StoredAuthCookie = {
	name: string;
	value: string;
	domain: string;
	path: string;
	expires: number;
	httpOnly: boolean;
	secure: boolean;
	sameSite: "Strict" | "Lax" | "None";
};

type BrowserCookie = {
	name: string;
	value: string;
	url: string;
	expires?: number;
	httpOnly: boolean;
	secure: boolean;
	sameSite: "Strict" | "Lax" | "None";
};

type StoredAuthState = {
	cookies?: StoredAuthCookie[];
};

/**
 * Check if a JWT token is still valid (not expired) without making a
 * network request.  Returns `true` when the token has at least 2 minutes
 * of remaining validity.
 */
function isTokenValid(token: string): boolean {
	try {
		const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
		// Require at least 10 minutes of remaining validity to ensure the token
		// lasts through the entire test run (which can take 7+ minutes)
		return typeof payload.exp === "number" && Date.now() / 1000 < payload.exp - 600;
	} catch {
		return false;
	}
}

function toBrowserCookie(setCookieHeader: string, baseURL: string): BrowserCookie | null {
	const segments = setCookieHeader
		.split(";")
		.map((segment) => segment.trim())
		.filter(Boolean);
	const [nameValue, ...attributes] = segments;
	if (!nameValue) {
		return null;
	}

	const separatorIndex = nameValue.indexOf("=");
	if (separatorIndex <= 0) {
		return null;
	}

	const cookie: BrowserCookie = {
		name: nameValue.slice(0, separatorIndex),
		value: nameValue.slice(separatorIndex + 1),
		url: baseURL,
		httpOnly: false,
		secure: false,
		sameSite: "Lax",
	};

	for (const attribute of attributes) {
		const [rawKey, ...rawValueParts] = attribute.split("=");
		const key = rawKey?.toLowerCase();
		const value = rawValueParts.join("=");

		switch (key) {
			case "expires": {
				const expiresAt = Date.parse(value);
				if (!Number.isNaN(expiresAt)) {
					cookie.expires = Math.floor(expiresAt / 1000);
				}
				break;
			}
			case "httponly":
				cookie.httpOnly = true;
				break;
			case "max-age": {
				const seconds = Number.parseInt(value, 10);
				if (Number.isFinite(seconds)) {
					cookie.expires = Math.floor(Date.now() / 1000) + seconds;
				}
				break;
			}
			case "path":
				// Playwright cookies must provide either url or domain/path.
				// This setup path uses url-based cookies for localhost auth.
				break;
			case "samesite":
				if (/^none$/i.test(value)) {
					cookie.sameSite = "None";
				} else if (/^strict$/i.test(value)) {
					cookie.sameSite = "Strict";
				} else {
					cookie.sameSite = "Lax";
				}
				break;
			case "secure":
				cookie.secure = true;
				break;
		}
	}

	return cookie;
}

async function syncResponseCookiesToBrowserContext(page: Page, baseURL: string, response: APIResponse): Promise<void> {
	const cookies = response
		.headersArray()
		.filter((header) => header.name.toLowerCase() === "set-cookie")
		.map((header) => toBrowserCookie(header.value, baseURL))
		.filter((cookie): cookie is BrowserCookie => cookie !== null);

	if (cookies.length > 0) {
		await page.context().addCookies(cookies);
	}
}

/**
 * Global setup: ensure a test user exists and persist authenticated state.
 * Runs once before all test projects.
 *
 * Strategy:
 * 1. If a valid auth file exists whose access_token JWT has not expired,
 *    reuse it without any network call (saves rate-limit budget).
 * 2. If auth is disabled (no login page), save state immediately.
 * 3. Try to register via API (idempotent — fails silently if user exists).
 * 4. Log in via the UI.
 */
setup("authenticate", async ({ page }) => {
	setup.setTimeout(120000);
	await applyVideoSafetyMode(page);
	const baseURL = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:5173";

	// Create .auth directory if it doesn't exist
	const authDir = path.dirname(authFile);
	if (!fs.existsSync(authDir)) {
		fs.mkdirSync(authDir, { recursive: true });
	}

	// ---- 1. Try to reuse an existing auth file (offline check only) ----
	if (fs.existsSync(authFile)) {
		try {
			const saved = JSON.parse(fs.readFileSync(authFile, "utf-8")) as StoredAuthState;
			const accessCookie = saved.cookies?.find((c: { name: string }) => c.name === "access_token");
			const refreshCookie = saved.cookies?.find((c: { name: string }) => c.name === "refresh_token");

			if (saved.cookies?.length) {
				await page.context().addCookies(saved.cookies);
			}

			if (accessCookie?.value && isTokenValid(accessCookie.value)) {
				const hasSavedSession = await page.request
					.get(`${baseURL}/api/auth/me`)
					.then((response) => response.ok())
					.catch(() => false);

				if (hasSavedSession) {
					await page.context().storageState({ path: authFile });
					return;
				}
			}

			if (refreshCookie?.value) {
				const refreshResponse = await page.request.post(`${baseURL}/api/auth/refresh`).catch(() => null);
				if (refreshResponse?.ok()) {
					await syncResponseCookiesToBrowserContext(page, baseURL, refreshResponse);

					const refreshedSession = await page.request
						.get(`${baseURL}/api/auth/me`)
						.then((response) => response.ok())
						.catch(() => false);

					if (refreshedSession) {
						await page.context().storageState({ path: authFile });
						return;
					}
				}
			}
		} catch {
			// Invalid file — fall through to regular login
		}
	}

	// ---- 2. Fast path: already authenticated session ----
	await page.goto("/");
	let authEnabled = true;
	let formLoginEnabled = true;
	let oidcEnabled = false;
	let registrationEnabled = true;
	try {
		const stateRes = await page.request.get(`${baseURL}/api/auth/state`);
		if (stateRes.ok()) {
			const state = await stateRes.json();
			authEnabled = state.authEnabled === true;
			formLoginEnabled = state.formLoginEnabled !== false;
			oidcEnabled = state.oidcEnabled === true;
			registrationEnabled = state.registrationEnabled !== false;
		}
	} catch {
		// Fallback: assume auth is enabled and form login is available.
	}

	// ---- 3. Check if auth is disabled ----
	if (!authEnabled) {
		await page.context().storageState({ path: authFile });
		return;
	}

	const hasUserMenu = await page
		.locator(".user-menu-btn")
		.isVisible({ timeout: 5000 })
		.catch(() => false);
	if (hasUserMenu) {
		await page.context().storageState({ path: authFile });
		return;
	}

	const hasAuthenticatedSession = await page.request
		.get(`${baseURL}/api/auth/me`)
		.then((response) => response.ok())
		.catch(() => false);
	if (hasAuthenticatedSession) {
		await page.goto("/");
		await expect(page.locator(".user-menu-btn")).toBeVisible({ timeout: 15000 });
		await page.context().storageState({ path: authFile });
		return;
	}

	const hasAuthContainer = await page
		.locator(".auth-container")
		.isVisible({ timeout: 5000 })
		.catch(() => false);
	if (!hasAuthContainer) {
		const hasLoginFields = await page
			.locator("#username")
			.isVisible({ timeout: 5000 })
			.catch(() => false);
		if (!hasLoginFields) {
			const becameAuthenticated = await page
				.locator("header.hero")
				.isVisible({ timeout: 5000 })
				.catch(() => false);
			if (becameAuthenticated) {
				await page.context().storageState({ path: authFile });
				return;
			}
		}
	}

	const loginWithApi = async () => {
		const res = await page.request.post(`${baseURL}/api/auth/login`, {
			data: { username: TEST_USER.username, password: TEST_USER.password, rememberMe: false },
		});

		if (res.ok()) {
			await syncResponseCookiesToBrowserContext(page, baseURL, res);
		}

		const bodyText = await res.text().catch(() => "");

		return {
			bodyText,
			ok: res.ok(),
			status: res.status(),
		};
	};

	const loginWithApiRetry = async (maxAttempts = 5) => {
		for (let attempt = 1; attempt <= maxAttempts; attempt++) {
			const result = await loginWithApi();
			if (result.ok) {
				return true;
			}

			const isRateLimited = result.status === 429 || /too many attempts/i.test(result.bodyText);
			if (!isRateLimited || attempt === maxAttempts) {
				return false;
			}

			await page.waitForTimeout(1000 * attempt);
		}

		return false;
	};

	const registerWithApi = async () => {
		await page.request
			.post(`${baseURL}/api/auth/register`, {
				data: { username: TEST_USER.username, password: TEST_USER.password },
			})
			.catch(() => {});
	};

	const ensureAuthenticated = async () => {
		const hasHeader = await page
			.locator("header.hero")
			.isVisible({ timeout: 8000 })
			.catch(() => false);
		if (hasHeader) return true;

		const meRes = await page.request.get(`${baseURL}/api/auth/me`).catch(() => null);
		return Boolean(meRes?.ok());
	};

	const hasBrowserAccessCookie = async () => {
		const cookies = await page.context().cookies(baseURL);
		return cookies.some((cookie) => cookie.name === "access_token");
	};

	// ---- 5. Log in via the appropriate method ----
	if (formLoginEnabled) {
		let loggedIn = await loginWithApiRetry();

		if (!loggedIn && registrationEnabled) {
			await registerWithApi();
			loggedIn = await loginWithApiRetry();
		}

		if (loggedIn && (await hasBrowserAccessCookie())) {
			await page.goto("/");
			const isAuthenticated = await ensureAuthenticated();
			if (!isAuthenticated) {
				throw new Error("Authentication succeeded but app shell did not become ready");
			}
			await page.context().storageState({ path: authFile });
			return;
		}

		// Fallback path for environments where API login flow is unavailable.
		const loginWithForm = async () => {
			const usernameField = page.locator("#username");
			const passwordField = page.locator("#password");

			// Make sure we're on the login form (not register)
			const isOnRegister = await page
				.locator(".auth-subtitle")
				.filter({ hasText: /Create Account/i })
				.isVisible()
				.catch(() => false);

			if (isOnRegister) {
				const switchBtn = page.locator("button.auth-link-btn");
				if (await switchBtn.isVisible().catch(() => false)) {
					await switchBtn.click();
					await page.waitForTimeout(500);
				}
			}

			await usernameField.clear();
			await usernameField.fill(TEST_USER.username);
			await passwordField.clear();
			await passwordField.fill(TEST_USER.password);

			// Click the submit button (not the SSO button)
			const submitButton = page.locator('button.auth-submit[type="submit"]');
			await expect(submitButton).toBeEnabled({ timeout: 15000 });
			await submitButton.click();
		};

		await loginWithForm();
		const hasHeroAfterFirstLogin = await page
			.locator("header.hero")
			.isVisible({ timeout: 5000 })
			.catch(() => false);

		if (!hasHeroAfterFirstLogin && registrationEnabled) {
			await registerWithApi();

			await loginWithForm();
		}
	} else if (oidcEnabled) {
		// SSO-only path: click the SSO button and let the OIDC provider handle login.
		// This requires the OIDC provider to be configured with test credentials
		// (e.g. via PLAYWRIGHT_OIDC_USERNAME / PLAYWRIGHT_OIDC_PASSWORD env vars)
		// or to auto-approve the test user.
		await page.locator("button.sso-btn").click();

		// Wait for OIDC redirect and callback — the provider may show its own login form
		const oidcUsername = process.env.PLAYWRIGHT_OIDC_USERNAME;
		const oidcPassword = process.env.PLAYWRIGHT_OIDC_PASSWORD;
		if (oidcUsername && oidcPassword) {
			// Fill OIDC provider login form (generic selectors — override if needed)
			await page.waitForURL(/.*/, { timeout: 15000 });
			const oidcUserField = page.locator('input[name="username"], input[name="login"], input[type="email"]').first();
			const oidcPassField = page.locator('input[name="password"], input[type="password"]').first();
			if (await oidcUserField.isVisible({ timeout: 10000 }).catch(() => false)) {
				await oidcUserField.fill(oidcUsername);
				await oidcPassField.fill(oidcPassword);
				await page.locator('button[type="submit"]').first().click();
			}
		}
	} else {
		throw new Error("No login method available: form login and OIDC are both disabled");
	}

	// Wait for successful auth. Prefer app header visibility, but allow verified
	// authenticated API state for environments where shell render is delayed.
	const isAuthenticated = await ensureAuthenticated();
	if (!isAuthenticated) {
		throw new Error("Authentication completed but no authenticated app state was detected");
	}

	// Persist authenticated state for all test projects
	await page.context().storageState({ path: authFile });
});
