import fs from "node:fs";
import { randomUUID } from "node:crypto";
import path from "node:path";
import { type Page, type TestInfo } from "@playwright/test";
import { expect, test } from "./test";
import { gotoAndWaitForAppReady } from "./helpers/page";

const testDataPath = path.join(process.cwd(), "playwright", "temp");

type RepositorySummary = {
	name: string;
	shortId: string;
};

type MirrorAssignmentSummary = {
	repositoryId: string;
	lastCopyStatus: "success" | "error" | "in_progress" | null;
};

type MirrorSyncStatus = {
	sourceCount: number;
	mirrorCount: number;
	missingSnapshots: unknown[];
};

function getRunId(testInfo: TestInfo) {
	return `${testInfo.parallelIndex}-${testInfo.retry}-${randomUUID().slice(0, 8)}`;
}

function getBackupShortId(backupPageUrl: string) {
	const backupShortId = new URL(backupPageUrl).pathname.split("/").filter(Boolean).at(-1);
	expect(backupShortId).toBeTruthy();
	return backupShortId!;
}

function getWorkerTestDataPath() {
	fs.mkdirSync(testDataPath, { recursive: true });
	return testDataPath;
}

async function listRepositories(page: Page) {
	const response = await page.request.get("/api/v1/repositories");
	expect(response.ok()).toBe(true);
	return (await response.json()) as RepositorySummary[];
}

async function getRepositoryShortId(page: Page, name: string) {
	const repositories = await listRepositories(page);
	const repository = repositories.find((entry) => entry.name === name);
	expect(repository).toBeDefined();
	return repository!.shortId;
}

async function createRepository(page: Page, name: string) {
	await gotoAndWaitForAppReady(page, "/repositories/create");
	await expect(page.getByRole("textbox", { name: "Name" })).toBeVisible();
	await page.getByRole("textbox", { name: "Name" }).fill(name);
	await page.getByRole("combobox", { name: "Backend" }).click();
	await page.getByRole("option", { name: "Local" }).click();
	await page.getByRole("button", { name: "Create repository" }).click();
	await expect(page.getByText("Repository created successfully")).toBeVisible({ timeout: 30000 });
}

async function createVolume(page: Page, name: string) {
	await gotoAndWaitForAppReady(page, "/volumes");
	const volumeNameInput = page.getByRole("textbox", { name: "Name" });
	await expect(async () => {
		await page.getByRole("button", { name: "Create Volume" }).click();
		await expect(volumeNameInput).toBeVisible();
	}).toPass({ timeout: 10000 });
	await volumeNameInput.fill(name);
	await page.getByRole("button", { name: "test-data" }).click();
	await page.getByRole("button", { name: "Create Volume" }).click();
	await expect(page.getByText("Volume created successfully")).toBeVisible();
}

async function createBackupJob(page: Page, backupName: string, volumeName: string, repositoryName: string) {
	await gotoAndWaitForAppReady(page, "/backups");
	const createBackupButton = page.getByRole("button", { name: "Create a backup job" }).first();
	if (await createBackupButton.isVisible()) {
		await createBackupButton.click();
	} else {
		await page.getByRole("link", { name: "Create a backup job" }).first().click();
	}
	const volumeSelect = page.getByRole("combobox").filter({ hasText: "Choose a volume to backup" });
	const volumeOption = page.getByRole("option", { name: volumeName });
	await expect(async () => {
		await volumeSelect.click();
		await expect(volumeOption).toBeVisible();
	}).toPass({ timeout: 10000 });
	await volumeOption.click();
	await page.getByRole("textbox", { name: "Backup name" }).fill(backupName);
	await page.getByRole("combobox").filter({ hasText: "Select a repository" }).click();
	await page.getByRole("option", { name: repositoryName }).click();
	await page.getByRole("combobox").filter({ hasText: "Select frequency" }).click();
	await page.getByRole("option", { name: "Daily" }).click();
	await page.getByRole("textbox", { name: "Execution time" }).fill("00:00");
	await page.getByRole("button", { name: "Create" }).click();
	await expect(page.getByText("Backup job created successfully")).toBeVisible();
}

async function waitForMirrorSyncComplete(page: Page, backupShortId: string, mirrorRepoShortId: string) {
	await expect(async () => {
		const mirrorsResponse = await page.request.get(`/api/v1/backups/${backupShortId}/mirrors`);
		expect(mirrorsResponse.ok()).toBe(true);
		const mirrors = (await mirrorsResponse.json()) as MirrorAssignmentSummary[];
		const mirror = mirrors.find((entry) => entry.repositoryId === mirrorRepoShortId);
		expect(mirror?.lastCopyStatus).toBe("success");

		const statusResponse = await page.request.get(
			`/api/v1/backups/${backupShortId}/mirrors/${mirrorRepoShortId}/status`,
		);
		expect(statusResponse.ok()).toBe(true);
		const status = (await statusResponse.json()) as MirrorSyncStatus;
		expect(status.sourceCount).toBe(1);
		expect(status.mirrorCount).toBe(1);
		expect(status.missingSnapshots).toHaveLength(0);
	}).toPass({ timeout: 30000 });
}

test("can sync missing snapshots to a mirror repository", async ({ page }, testInfo) => {
	const runId = getRunId(testInfo);
	const volumeName = `Volume-${runId}`;
	const primaryRepoName = `Primary-${runId}`;
	const mirrorRepoName = `Mirror-${runId}`;
	const backupName = `Backup-${runId}`;

	const workerTestDataPath = getWorkerTestDataPath();
	const runPath = path.join(workerTestDataPath, runId);
	fs.mkdirSync(runPath, { recursive: true });
	fs.writeFileSync(path.join(runPath, "test.json"), JSON.stringify({ data: "mirror sync test" }));

	await gotoAndWaitForAppReady(page, "/");
	await expect(page).toHaveURL("/volumes");

	// Create volume, two repositories, and a backup job
	await createVolume(page, volumeName);
	await createRepository(page, primaryRepoName);
	await createRepository(page, mirrorRepoName);
	await createBackupJob(page, backupName, volumeName, primaryRepoName);

	const backupPageUrl = page.url();
	const backupShortId = getBackupShortId(backupPageUrl);
	const primaryRepoShortId = await getRepositoryShortId(page, primaryRepoName);
	const mirrorRepoShortId = await getRepositoryShortId(page, mirrorRepoName);

	// Run a backup to create a snapshot and wait for it to persist.
	await page.getByRole("button", { name: "Backup now" }).click();
	await gotoAndWaitForAppReady(page, `/repositories/${primaryRepoShortId}`);
	await page.getByRole("tab", { name: "Snapshots" }).click();
	await expect(page.getByText("Backup snapshots stored in this repository.")).toBeVisible();
	await expect(async () => {
		await page.getByRole("button", { name: "Refresh" }).click();
		await expect(page.getByRole("checkbox", { name: /Select snapshot/ })).toHaveCount(1);
	}).toPass({ timeout: 30000 });
	await gotoAndWaitForAppReady(page, backupPageUrl);

	// Add mirror repository
	await page.getByRole("button", { name: "Add mirror" }).click();
	const mirrorSelect = page.getByRole("combobox").filter({ hasText: "Select a repository to mirror to..." });
	await mirrorSelect.click();
	await page.getByRole("option", { name: mirrorRepoName }).click();
	await page.getByRole("button", { name: "Save changes" }).click();
	await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0);

	// Click sync button on the mirror row (first icon button in the actions cell)
	const mirrorRow = page.getByRole("row").filter({ hasText: mirrorRepoName });
	await expect(mirrorRow).toBeVisible();
	await mirrorRow.getByRole("button").first().click();

	// Verify the sync dialog shows missing snapshots
	const syncDialog = page.getByRole("dialog");
	await expect(syncDialog.getByRole("heading", { name: "Sync snapshots" })).toBeVisible();
	await expect(syncDialog.getByText(/1 of 1 snapshots are missing/)).toBeVisible({ timeout: 15000 });

	// Select the missing snapshots explicitly before syncing.
	await expect(syncDialog.getByRole("button", { name: "Sync 0 snapshots" })).toBeDisabled();
	await syncDialog.getByRole("checkbox").first().click();
	const syncButton = syncDialog.getByRole("button", { name: "Sync 1 snapshots" });
	await expect(syncButton).toBeEnabled();

	// Click sync and wait on the mirror status that backs this dialog and row.
	await syncButton.click();
	await waitForMirrorSyncComplete(page, backupShortId, mirrorRepoShortId);
	await gotoAndWaitForAppReady(page, backupPageUrl);

	// Open sync dialog again and verify all snapshots are synced
	await expect(async () => {
		await expect(mirrorRow).toBeVisible();
		await mirrorRow.getByRole("button").first().click();
		await expect(syncDialog.getByRole("heading", { name: "Sync snapshots" })).toBeVisible();
	}).toPass({ timeout: 15000 });
	await expect(syncDialog.getByText(/All 1 snapshots are already synced/)).toBeVisible({ timeout: 15000 });
	await syncDialog.getByRole("button", { name: "Cancel" }).click();

	// Verify the synced snapshot remains visible in the mirror repository UI.
	await gotoAndWaitForAppReady(page, `/repositories/${mirrorRepoShortId}`);
	await page.getByRole("tab", { name: "Snapshots" }).click();
	await expect(page.getByRole("checkbox", { name: /Select snapshot/ })).toHaveCount(1);
});
