import { spawn } from "node:child_process";
import { rmSync } from "node:fs";
import { resolve } from "node:path";

function delay(ms: number): Promise<void> {
    return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
}

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

async function waitForHealth(baseUrl: string, timeoutMs: number): Promise<void> {
    const start = Date.now();
    while (Date.now() - start < timeoutMs) {
        try {
            const response = await fetch(`${baseUrl}/health`);
            if (response.status === 200 || response.status === 503) {
                return;
            }
        } catch {
            // retry
        }
        await delay(100);
    }
    throw new Error("Timed out waiting for server startup.");
}

async function getJson(path: string): Promise<{
    status: number;
    data: Record<string, unknown>;
    response: Response;
}> {
    const response = await fetch(path);
    const data = (await response.json().catch(() => ({}))) as Record<string, unknown>;
    return { status: response.status, data, response };
}

async function postJson(
    path: string,
    payload: Record<string, unknown>
): Promise<{ status: number; data: Record<string, unknown>; response: Response }> {
    const response = await fetch(path, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(payload)
    });
    const data = (await response.json().catch(() => ({}))) as Record<string, unknown>;
    return { status: response.status, data, response };
}

async function main(): Promise<void> {
    const host = "127.0.0.1";
    const port = 9000 + Math.floor(Math.random() * 500);
    const baseUrl = `http://${host}:${port}`;
    const smokeDataFile = resolve(
        process.cwd(),
        "data",
        `phase16-smoke-${process.pid}-${Date.now()}.json`
    );
    const smokeChatDir = resolve(
        process.cwd(),
        "data",
        `phase16-chat-${process.pid}-${Date.now()}`
    );
    const setupKeyA = "phase16_setup_key_a";
    const setupKeyB = "phase16_setup_key_b";

    const previousDataFile = process.env.DATA_FILE;
    const previousChatLogDir = process.env.CHAT_LOG_DIR;
    const previousNodeEnv = process.env.NODE_ENV;
    const previousTokenPepper = process.env.TOKEN_PEPPER;
    const previousHost = process.env.HOST;
    const previousPort = process.env.PORT;
    const previousSetupKeys = process.env.SETUP_KEYS;

    let child: ReturnType<typeof spawn> | null = null;

    try {
        child = spawn(
            process.execPath,
            [resolve(process.cwd(), "node_modules/tsx/dist/cli.mjs"), "src/index.ts"],
            {
                cwd: process.cwd(),
                env: {
                    ...process.env,
                    NODE_ENV: "production",
                    HOST: host,
                    PORT: String(port),
                    TOKEN_PEPPER: "phase16-production-test-pepper",
                    DATA_FILE: smokeDataFile,
                    CHAT_LOG_DIR: smokeChatDir,
                    SETUP_KEYS: `${setupKeyA},${setupKeyB}`
                },
                stdio: ["ignore", "pipe", "pipe"]
            }
        );

        await waitForHealth(baseUrl, 12000);

        const robots = await fetch(`${baseUrl}/robots.txt`);
        assert(robots.status === 200, "Expected robots.txt route to be available.");
        assert(
            (await robots.text()).includes("Disallow: /"),
            "Expected robots.txt to disallow all crawlers."
        );
        assert(
            robots.headers.get("x-robots-tag") === "noindex, nofollow, noarchive",
            "Expected robots.txt response to include no-index header."
        );

        const statusBefore = await getJson(`${baseUrl}/setup/status`);
        assert(statusBefore.status === 200, "Expected setup status endpoint to be available.");
        assert(
            statusBefore.response.headers.get("cache-control") === "no-store",
            "Expected setup status API to include no-store cache policy."
        );
        assert(statusBefore.data.setupAvailable === true, "Expected setup to be available before completion.");

        const setupPageBefore = await fetch(`${baseUrl}/setup`);
        assert(setupPageBefore.status === 200, "Expected setup page to be available before setup locks.");
        assert(
            setupPageBefore.headers.get("cache-control") === "no-store",
            "Expected setup page to include no-store cache policy."
        );
        assert(
            (await setupPageBefore.text()).includes("Conduit Setup"),
            "Expected setup page content before setup locks."
        );

        const invalidAuth = await postJson(`${baseUrl}/setup/auth`, {
            setupKey: "not-valid"
        });
        assert(invalidAuth.status === 401, "Expected invalid setup key to fail.");

        const authOk = await postJson(`${baseUrl}/setup/auth`, { setupKey: setupKeyA });
        assert(authOk.status === 200, "Expected setup auth success for valid key.");
        const setupSessionToken = String(authOk.data.setupSessionToken ?? "");
        assert(setupSessionToken.length > 10, "Expected setup session token.");

        const invalidSessionComplete = await postJson(`${baseUrl}/setup/complete`, {
            setupSessionToken: "invalid-session",
            streamName: "phase16 stream"
        });
        assert(
            invalidSessionComplete.status === 401,
            "Expected setup completion to reject invalid setup session."
        );

        const completeOk = await postJson(`${baseUrl}/setup/complete`, {
            setupSessionToken,
            streamName: "phase16 stream"
        });
        assert(completeOk.status === 200, "Expected setup completion success.");
        const adminUrl = String(completeOk.data.adminUrl ?? "");
        const overlayUrl = completeOk.data.overlayUrl;
        const adminToken = String(completeOk.data.adminToken ?? "");
        assert(adminUrl.endsWith("/admin"), "Expected admin URL without query token.");
        assert(overlayUrl === null, "Expected no overlay URL in production setup.");
        assert(adminToken.startsWith("cdt_"), "Expected issued admin token.");

        const statusAfterFirstCompletion = await getJson(`${baseUrl}/setup/status`);
        assert(
            statusAfterFirstCompletion.data.setupLocked === false,
            "Expected setup to remain unlocked while unused setup keys remain."
        );
        assert(
            statusAfterFirstCompletion.data.setupAvailable === true,
            "Expected setup to remain available while unused setup keys remain."
        );
        assert(
            Number(statusAfterFirstCompletion.data.usedKeyCount ?? 0) === 1,
            "Expected one used setup key after first completion."
        );

        const authWithSecondKey = await postJson(`${baseUrl}/setup/auth`, { setupKey: setupKeyB });
        assert(authWithSecondKey.status === 200, "Expected second setup key to authenticate.");
        const secondSetupSessionToken = String(authWithSecondKey.data.setupSessionToken ?? "");
        assert(secondSetupSessionToken.length > 10, "Expected second setup session token.");

        const completeSecond = await postJson(`${baseUrl}/setup/complete`, {
            setupSessionToken: secondSetupSessionToken,
            streamName: "phase16 stream 2"
        });
        assert(completeSecond.status === 200, "Expected second setup key completion to succeed.");

        const secondAttempt = await postJson(`${baseUrl}/setup/auth`, { setupKey: setupKeyA });
        assert(
            secondAttempt.status === 423,
            "Expected setup auth blocked after all setup keys are consumed."
        );

        const statusAfter = await getJson(`${baseUrl}/setup/status`);
        assert(statusAfter.status === 200, "Expected setup status after completion.");
        assert(statusAfter.data.setupLocked === true, "Expected setup to be locked once all keys are consumed.");
        assert(
            statusAfter.data.setupAvailable === false,
            "Expected setup to be unavailable once all keys are consumed."
        );
        assert(Number(statusAfter.data.usedKeyCount ?? 0) === 2, "Expected both setup keys to be marked as used.");

        const adminPage = await fetch(`${baseUrl}/admin`);
        assert(adminPage.status === 200, "Expected admin page route to remain available.");
        assert(
            adminPage.headers.get("cache-control") === "no-store",
            "Expected admin page to include no-store cache policy."
        );
        assert(
            !(await adminPage.clone().text()).includes("First-Time Setup Wizard"),
            "Expected admin page not to include setup wizard markup."
        );
        assert(
            (await adminPage.clone().text()).includes("Admin Token"),
            "Expected admin page to include admin token prompt."
        );
        assert(
            adminPage.headers.get("x-robots-tag") === "noindex, nofollow, noarchive",
            "Expected admin page to include no-index header."
        );
        assert(
            adminPage.headers.get("x-content-type-options") === "nosniff",
            "Expected admin page to include nosniff header."
        );
        assert(
            adminPage.headers.get("referrer-policy") === "no-referrer",
            "Expected admin page to include no-referrer policy."
        );
        assert(
            Boolean(adminPage.headers.get("content-security-policy")?.includes("default-src 'none'")),
            "Expected admin page to include content security policy."
        );

        const setupPageAfter = await fetch(`${baseUrl}/setup`);
        assert(setupPageAfter.status === 404, "Expected setup page to be unavailable after setup locks.");
        assert(
            setupPageAfter.headers.get("cache-control") === "no-store",
            "Expected locked setup response to include no-store cache policy."
        );

        const overlayPage = await fetch(`${baseUrl}/overlay`);
        assert(overlayPage.status === 404, "Expected overlay page to be unavailable in production.");
        assert(
            overlayPage.headers.get("cache-control") === "no-store",
            "Expected production overlay response to include no-store cache policy."
        );

        console.log(
            JSON.stringify(
                {
                    ok: true,
                    setupLocked: statusAfter.data.setupLocked,
                    adminUrl,
                    overlayUrl
                },
                null,
                2
            )
        );
    } finally {
        if (child) {
            child.kill("SIGTERM");
            await delay(250);
            if (!child.killed) {
                child.kill("SIGKILL");
            }
        }

        if (previousDataFile === undefined) {
            delete process.env.DATA_FILE;
        } else {
            process.env.DATA_FILE = previousDataFile;
        }
        if (previousNodeEnv === undefined) {
            delete process.env.NODE_ENV;
        } else {
            process.env.NODE_ENV = previousNodeEnv;
        }
        if (previousTokenPepper === undefined) {
            delete process.env.TOKEN_PEPPER;
        } else {
            process.env.TOKEN_PEPPER = previousTokenPepper;
        }
        if (previousChatLogDir === undefined) {
            delete process.env.CHAT_LOG_DIR;
        } else {
            process.env.CHAT_LOG_DIR = previousChatLogDir;
        }
        if (previousHost === undefined) {
            delete process.env.HOST;
        } else {
            process.env.HOST = previousHost;
        }
        if (previousPort === undefined) {
            delete process.env.PORT;
        } else {
            process.env.PORT = previousPort;
        }
        if (previousSetupKeys === undefined) {
            delete process.env.SETUP_KEYS;
        } else {
            process.env.SETUP_KEYS = previousSetupKeys;
        }

        rmSync(smokeDataFile, { force: true });
        rmSync(smokeChatDir, { force: true, recursive: true });
    }
}

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