import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { Page } from "playwright";
import { evalObsidianJson } from "../runner/cli.ts";
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
import {
    createE2eObsidianDeviceLocalState,
    waitForLiveSyncCoreReady,
    waitForLocalDatabaseEntry,
} from "../runner/liveSyncWorkflow.ts";
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
import { withObsidianPage } from "../runner/ui.ts";
import { createTemporaryVault } from "../runner/vault.ts";

process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "60000";
process.env.E2E_OBSIDIAN_CORE_READY_TIMEOUT_MS ??= "60000";
process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ??= "30000";

const notePath = "E2E/document-history-soft-deleted.md";
const contentMarker = "Recoverable content from the soft-deleted revision";
const noteContent = [
    "# Document History recovery E2E",
    "",
    contentMarker,
    "",
    ...Array.from(
        { length: 96 },
        (_, index) => `Preserved content line ${String(index + 1).padStart(3, "0")}: ${"R".repeat(64)}`
    ),
    "",
].join("\n");

type SoftDeletionState = {
    id: string;
    revision: string;
    revisionCount: number;
    chunkReferences: number;
    availableChunks: number;
    contentReadable: boolean;
    storageExists: boolean;
};

type VaultRestoreState = {
    revision: string;
    revisionCount: number;
    deleted: boolean;
    storageExists: boolean;
    contentMatches: boolean;
};

function assertEqual(actual: unknown, expected: unknown, message: string): void {
    if (actual !== expected) {
        throw new Error(`${message}\nExpected: ${String(expected)}\nActual: ${String(actual)}`);
    }
}

function assertTrue(value: boolean, message: string): void {
    if (!value) {
        throw new Error(message);
    }
}

async function dismissWelcomeWizard(port: number): Promise<void> {
    await withObsidianPage(port, async (page) => {
        const cancel = page.getByText("No, please take me back");
        if (await cancel.isVisible({ timeout: 5000 }).catch(() => false)) {
            await cancel.click();
            await page.waitForTimeout(500);
        }
    });
}

async function createNote(cliBinary: string, env: NodeJS.ProcessEnv): Promise<void> {
    await evalObsidianJson<unknown>(
        cliBinary,
        [
            "(async()=>{",
            `const path=${JSON.stringify(notePath)};`,
            `const content=${JSON.stringify(noteContent)};`,
            "if(!(await app.vault.adapter.exists('E2E'))) await app.vault.createFolder('E2E');",
            "const existing=app.vault.getAbstractFileByPath(path);",
            "if(existing) await app.vault.delete(existing);",
            "const file=await app.vault.create(path,content);",
            "await app.workspace.getLeaf(false).openFile(file);",
            "return JSON.stringify({ok:true});",
            "})()",
        ].join(""),
        env
    );
    await waitForLocalDatabaseEntry(cliBinary, env, notePath);
}

async function createSoftDeletion(cliBinary: string, env: NodeJS.ProcessEnv): Promise<SoftDeletionState> {
    return await evalObsidianJson<SoftDeletionState>(
        cliBinary,
        [
            "(async()=>{",
            `const path=${JSON.stringify(notePath)};`,
            `const expectedContent=${JSON.stringify(noteContent)};`,
            "const timeoutMs=30000;",
            "const core=app.plugins.plugins['obsidian-livesync'].core;",
            "const file=app.vault.getAbstractFileByPath(path);",
            "if(!file) throw new Error(`Recovery fixture is missing from the Vault: ${path}`);",
            "const id=await core.services.path.path2id(path);",
            "await app.vault.delete(file);",
            "const deadline=Date.now()+timeoutMs;",
            "const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));",
            "while(Date.now()<deadline){",
            "  await core.services.fileProcessing.commitPendingFileEvents();",
            "  const raw=await core.localDatabase.getRaw(id,{revs_info:true}).catch(()=>false);",
            "  if(raw?.deleted&&!app.vault.getAbstractFileByPath(path)){",
            "    const loaded=await core.localDatabase.getDBEntry(path,{rev:raw._rev},false,true,true);",
            "    const loadedContent=loaded===false?'':Array.isArray(loaded.data)?loaded.data.join(''):loaded.data;",
            "    const children=Array.isArray(raw.children)?raw.children:[];",
            "    const chunkRows=children.length===0?{rows:[]}:await core.localDatabase.allDocsRaw({keys:children,include_docs:true});",
            "    const availableChunks=chunkRows.rows.filter((row)=>row.doc&&!row.value?.deleted).length;",
            "    return JSON.stringify({",
            "      id,",
            "      revision:raw._rev,",
            "      revisionCount:(raw._revs_info||[]).filter((entry)=>entry?.status==='available').length,",
            "      chunkReferences:children.length,",
            "      availableChunks,",
            "      contentReadable:loadedContent===expectedContent,",
            "      storageExists:!!app.vault.getAbstractFileByPath(path),",
            "    });",
            "  }",
            "  await sleep(250);",
            "}",
            "throw new Error(`Timed out waiting for a readable soft deletion: ${path}`);",
            "})()",
        ].join(""),
        env
    );
}

async function openHistoryPicker(cliBinary: string, env: NodeJS.ProcessEnv): Promise<void> {
    await evalObsidianJson<unknown>(
        cliBinary,
        [
            "(async()=>{",
            "document.querySelectorAll('.modal-close-button').forEach((button)=>button.click());",
            "await new Promise((resolve)=>setTimeout(resolve,300));",
            "await app.commands.executeCommandById('obsidian-livesync:livesync-filehistory');",
            "await new Promise((resolve)=>setTimeout(resolve,500));",
            "return JSON.stringify({opened:!!document.querySelector('.prompt-input')});",
            "})()",
        ].join(""),
        env
    );
}

async function waitForVaultRestore(cliBinary: string, env: NodeJS.ProcessEnv): Promise<VaultRestoreState> {
    return await evalObsidianJson<VaultRestoreState>(
        cliBinary,
        [
            "(async()=>{",
            `const path=${JSON.stringify(notePath)};`,
            `const expectedContent=${JSON.stringify(noteContent)};`,
            "const timeoutMs=30000;",
            "const core=app.plugins.plugins['obsidian-livesync'].core;",
            "const id=await core.services.path.path2id(path);",
            "const deadline=Date.now()+timeoutMs;",
            "const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));",
            "while(Date.now()<deadline){",
            "  await core.services.fileProcessing.commitPendingFileEvents();",
            "  const file=app.vault.getAbstractFileByPath(path);",
            "  const raw=await core.localDatabase.getRaw(id,{revs_info:true}).catch(()=>false);",
            "  const content=file?await app.vault.read(file):'';",
            "  if(file&&raw&&!raw.deleted&&!raw._deleted&&content===expectedContent){",
            "    return JSON.stringify({",
            "      revision:raw._rev,",
            "      revisionCount:(raw._revs_info||[]).filter((entry)=>entry?.status==='available').length,",
            "      deleted:false,",
            "      storageExists:true,",
            "      contentMatches:true,",
            "    });",
            "  }",
            "  await sleep(250);",
            "}",
            "const file=app.vault.getAbstractFileByPath(path);",
            "const raw=await core.localDatabase.getRaw(id,{revs_info:true}).catch(()=>false);",
            "const content=file?await app.vault.read(file):'';",
            "throw new Error(`Timed out waiting for History to create and reflect a non-deleted successor revision: ${JSON.stringify({storageExists:!!file,deleted:!!(raw&&((raw.deleted||raw._deleted))),contentMatches:content===expectedContent,revision:raw&&raw._rev})}`);",
            "})()",
        ].join(""),
        env
    );
}

async function openActiveFileHistory(cliBinary: string, env: NodeJS.ProcessEnv): Promise<void> {
    await evalObsidianJson<unknown>(
        cliBinary,
        [
            "(async()=>{",
            `const path=${JSON.stringify(notePath)};`,
            "document.querySelectorAll('.modal-close-button').forEach((button)=>button.click());",
            "const file=app.vault.getAbstractFileByPath(path);",
            "if(!file) throw new Error(`Restored file is missing before reopening history: ${path}`);",
            "await app.workspace.getLeaf(false).openFile(file);",
            "await new Promise((resolve)=>setTimeout(resolve,300));",
            "await app.commands.executeCommandById('obsidian-livesync:livesync-history');",
            "await new Promise((resolve)=>setTimeout(resolve,500));",
            "return JSON.stringify({opened:!!document.querySelector('.modal-container .modal-title')});",
            "})()",
        ].join(""),
        env
    );
}

async function captureStep(page: Page, screenshotDir: string, step: string): Promise<string> {
    await mkdir(screenshotDir, { recursive: true });
    const path = join(screenshotDir, `${step}.png`);
    await page.screenshot({ path, fullPage: true, animations: "disabled" });
    console.log(`Screenshot: ${path}`);
    return path;
}

async function main(): Promise<void> {
    const binary = requireObsidianBinary();
    const cli = discoverObsidianCli();
    if (!cli.binary) throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`);

    const vault = await createTemporaryVault();
    let session: ObsidianLiveSyncSession | undefined;
    const screenshotDir =
        process.env.E2E_OBSIDIAN_HISTORY_RESTORE_SCREENSHOT_DIR ??
        join(process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e", "document-history-restore");
    const reportPath =
        process.env.E2E_OBSIDIAN_HISTORY_RESTORE_REPORT ?? join(screenshotDir, "document-history-restore.json");

    try {
        console.log(`Using Obsidian executable: ${binary}`);
        console.log(`Temporary vault: ${vault.path}`);

        session = await startObsidianLiveSyncSession({
            binary,
            cliBinary: cli.binary,
            vault,
            startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
            pluginData: {
                doctorProcessedVersion: "1.0.0",
                isConfigured: true,
                liveSync: false,
                remoteType: "",
                couchDB_URI: "",
                couchDB_DBNAME: "",
                couchDB_USER: "",
                couchDB_PASSWORD: "",
                remoteConfigurations: {},
                activeConfigurationId: "",
                notifyThresholdOfRemoteStorageSize: -1,
                periodicReplication: false,
                syncAfterMerge: false,
                syncOnEditorSave: false,
                syncOnFileOpen: false,
                syncOnSave: false,
                syncOnStart: false,
                deleteMetadataOfDeletedFiles: false,
            },
            localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
        });
        await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
        await dismissWelcomeWizard(session.remoteDebuggingPort);

        await createNote(cli.binary, session.cliEnv);
        const deletion = await createSoftDeletion(cli.binary, session.cliEnv);
        assertEqual(deletion.storageExists, false, "The deletion fixture still existed in the Vault.");
        assertTrue(deletion.chunkReferences > 0, "The deleted document did not retain chunk references.");
        assertEqual(
            deletion.availableChunks,
            deletion.chunkReferences,
            "Not all chunks referenced by the deleted document remained available."
        );
        assertEqual(
            deletion.contentReadable,
            true,
            "The soft-deleted revision could not be reconstructed from chunks."
        );

        await openHistoryPicker(cli.binary, session.cliEnv);

        const screenshots = await withObsidianPage(session.remoteDebuggingPort, async (page) => {
            const screenshotPaths: string[] = [];
            const prompt = page.locator(".prompt");
            await prompt.waitFor({ state: "visible", timeout: 10000 });
            const promptInput = prompt.locator(".prompt-input");
            assertEqual(
                await promptInput.getAttribute("placeholder"),
                "File to view History",
                "Unexpected history picker placeholder."
            );
            await promptInput.fill(notePath);
            const suggestion = prompt.locator(".suggestion-item").filter({ hasText: notePath }).first();
            await suggestion.waitFor({ state: "visible", timeout: 10000 });
            screenshotPaths.push(await captureStep(page, screenshotDir, "01-soft-deleted-file-picker"));

            await suggestion.click();
            const modal = page.locator(".modal-container").filter({ hasText: "Document History" });
            await modal.waitFor({ state: "visible", timeout: 10000 });
            await modal.getByText("(At this revision, the file has been deleted)", { exact: false }).waitFor({
                state: "visible",
                timeout: 10000,
            });
            await modal.getByText(contentMarker, { exact: false }).waitFor({ state: "visible", timeout: 10000 });
            const restoreButton = modal.getByRole("button", { name: "Back to this revision", exact: true });
            await restoreButton.waitFor({ state: "visible", timeout: 10000 });
            screenshotPaths.push(await captureStep(page, screenshotDir, "02-readable-deleted-revision"));

            await restoreButton.click();
            await modal.waitFor({ state: "hidden", timeout: 10000 });
            return screenshotPaths;
        });

        const restored = await waitForVaultRestore(cli.binary, session.cliEnv);
        assertEqual(restored.storageExists, true, "Document History did not restore the Vault file.");
        assertEqual(restored.contentMatches, true, "The restored Vault file did not match the deleted revision.");
        assertEqual(restored.deleted, false, "Document History did not produce a non-deleted database revision.");
        assertTrue(
            restored.revision !== deletion.revision,
            "Document History did not create a successor database revision."
        );
        assertEqual(
            restored.revisionCount,
            deletion.revisionCount + 1,
            "Document History did not add exactly one non-deleted successor revision."
        );

        screenshots.push(
            await withObsidianPage(session.remoteDebuggingPort, async (page) => {
                await page
                    .getByText(contentMarker, { exact: false })
                    .first()
                    .waitFor({ state: "visible", timeout: 10000 });
                return await captureStep(page, screenshotDir, "03-non-deleted-successor-after-history-restore");
            })
        );

        await openActiveFileHistory(cli.binary, session.cliEnv);
        screenshots.push(
            await withObsidianPage(session.remoteDebuggingPort, async (page) => {
                const modal = page.locator(".modal-container").filter({ hasText: "Document History" });
                await modal.waitFor({ state: "visible", timeout: 10000 });
                await modal.getByText(contentMarker, { exact: false }).waitFor({ state: "visible", timeout: 10000 });
                assertEqual(
                    await modal.getByText("(At this revision, the file has been deleted)", { exact: false }).count(),
                    0,
                    "The non-deleted successor revision was still displayed as deleted."
                );
                assertEqual(
                    (await modal.locator(".history-rev-indicator").innerText()).trim(),
                    "Rev 3/3",
                    "Document History did not open at the new non-deleted successor revision."
                );
                return await captureStep(page, screenshotDir, "04-restored-revision-in-history");
            })
        );

        await mkdir(screenshotDir, { recursive: true });
        await writeFile(
            reportPath,
            `${JSON.stringify({ notePath, deletion, restored, screenshots }, null, 2)}\n`,
            "utf-8"
        );
        console.log("Document History soft-deletion restoration E2E passed.");
        console.log(`Report: ${reportPath}`);
        console.log(`Screenshots: ${screenshotDir}`);
    } finally {
        if (session) await session.app.stop();
        await vault.dispose();
    }
}

main().catch((error: unknown) => {
    console.error(error instanceof Error ? error.stack : error);
    process.exit(1);
});
