const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { isExternalUrlAllowed } = require("../src/utils/swai_utils");
const { getManifestAllowedUrls } = require("../src/utils/WebAppManifest");
const WebApp = require("../src/utils/WebApp");
const { configureAppDataPaths } = require("../src/utils/AppDataPaths");
const { ensureWidevine } = require("../src/utils/WidevineManager");
const {
    getWidevineRelaunchCommand,
    scheduleWidevineRelaunch,
} = require("../src/utils/AppRelaunch");
const EcosystemLauncher = require("../src/managers/EcosystemLauncher");
const {
    findInstalledIcon,
    getLinuxAppIdentity,
} = require("../src/utils/LinuxAppIdentity");
const {
    getBusName,
    serializeWebApp,
    normalizeWebAppRequest,
} = require("../src/managers/EcosystemLauncher");

function testExternalUrls() {
    assert.strictEqual(isExternalUrlAllowed("https://example.com/page"), true);
    assert.strictEqual(isExternalUrlAllowed("mailto:person@example.com"), true);
    assert.strictEqual(isExternalUrlAllowed("tel:+15551234567"), true);
    assert.strictEqual(isExternalUrlAllowed("javascript:alert(1)"), false);
    assert.strictEqual(isExternalUrlAllowed("file:///etc/passwd"), false);
    assert.strictEqual(isExternalUrlAllowed("not a url"), false);
}

function testManifestUrls() {
    const urls = getManifestAllowedUrls(
        {
            scope: "/app/",
            start_url: "/app/home?source=manifest",
            scope_extensions: [
                { origin: "https://accounts.example.net" },
                "https://files.example.net/path",
                { origin: "javascript:alert(1)" },
                { origin: "https://*.example.net" },
            ],
        },
        "https://app.example.com/manifest.webmanifest",
        "https://app.example.com/app/",
    );

    assert.deepStrictEqual(urls, [
        "https://app.example.com/app/*",
        "https://app.example.com/app/home?source=manifest",
        "https://accounts.example.net/*",
        "https://files.example.net/*",
    ]);

    assert.deepStrictEqual(
        getManifestAllowedUrls(
            { scope: "https://attacker.example/", start_url: "https://attacker.example/" },
            "https://app.example.com/manifest.webmanifest",
            "https://app.example.com/",
        ),
        [],
    );
}

function testNavigationPatterns() {
    const { wildCardMatch, wildCardMatchList } = require("../src/utils/swai_utils");
    assert.ok(wildCardMatch("https://example.com/path", "https://example.com/*"));
    assert.ok(wildCardMatch("https://example.com", "https://example.com/*"));
    assert.ok(wildCardMatch("https://example.com/?q=a.b", "https://example.com/?q=a.b"));
    assert.strictEqual(
        wildCardMatch("https://example.com.attacker.test/", "https://example.com/*"),
        null,
    );
    assert.strictEqual(
        wildCardMatchList("https://login.example.com/start", ["https://login.*/*"]),
        true,
    );
}

function testWebAppConfiguration() {
    const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swai-tests-"));
    const sourcePath = path.join(tempDir, "source.swai");
    const outputPath = path.join(tempDir, "output.swai");
    fs.writeFileSync(sourcePath, [
        "app_id: com.example.test",
        "app_name: Test App",
        "main_url: https://example.com/",
        "allowed_urls:",
        "  - https://example.com/*",
        "allow_multi_window: false",
        "in_page_navigation_redirect: false",
        "use_manifest: true",
        "reuse_window_for_ecosystem_navigation: true",
    ].join("\n"));

    try {
        const webapp = WebApp.from_yaml_file(sourcePath);
        assert.strictEqual(webapp.allow_multi_window, false);
        assert.strictEqual(webapp.in_page_navigation_redirect, false);
        assert.strictEqual(webapp.use_manifest, true);
        assert.strictEqual(webapp.reuse_window_for_ecosystem_navigation, true);

        const clone = webapp.clone();
        assert.deepStrictEqual(clone.allowed_urls, webapp.allowed_urls);
        webapp.to_yaml_file(outputPath);
        assert.strictEqual(WebApp.from_yaml_file(outputPath).allow_multi_window, false);
        assert.strictEqual(
            WebApp.from_yaml_file(outputPath).reuse_window_for_ecosystem_navigation,
            true,
        );
    } finally {
        fs.rmSync(tempDir, { recursive: true, force: true });
    }
}

function testLinuxAppIdentity() {
    assert.deepStrictEqual(getLinuxAppIdentity("com.apple.music"), {
        id: "io.stillhq.swai.com.apple.music",
        desktopName: "io.stillhq.swai.com.apple.music",
    });

    const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swai-desktop-"));
    const oldDataHome = process.env.XDG_DATA_HOME;
    const iconPath = path.join(tempDir, "apple-music.png");
    const applicationsDir = path.join(tempDir, "applications");
    fs.mkdirSync(applicationsDir);
    fs.writeFileSync(iconPath, "test icon");
    fs.writeFileSync(
        path.join(applicationsDir, "io.stillhq.swai.com.apple.music.desktop"),
        `[Desktop Entry]\nIcon=${iconPath}\n`,
    );
    process.env.XDG_DATA_HOME = tempDir;
    try {
        assert.strictEqual(findInstalledIcon("com.apple.music"), iconPath);
    } finally {
        if (oldDataHome === undefined) delete process.env.XDG_DATA_HOME;
        else process.env.XDG_DATA_HOME = oldDataHome;
        fs.rmSync(tempDir, { recursive: true, force: true });
    }
}

async function testEcosystemProtocol() {
    const app = {
        app_id: "com.apple.music",
        app_name: "Apple Music",
        main_url: "https://music.apple.com/",
        allowed_urls: ["https://music.apple.com/*"],
        custom_js: "must not cross the IPC boundary",
        reuse_window_for_ecosystem_navigation: true,
    };

    assert.match(getBusName("apple/../../unsafe"), /^io\.stillhq\.Swai\.Instance\.i[0-9a-f]{32}$/);
    assert.notStrictEqual(getBusName("apple"), getBusName("music"));

    const serialized = serializeWebApp(app);
    const normalized = normalizeWebAppRequest(serialized);
    assert.strictEqual(normalized.app_id, app.app_id);
    assert.strictEqual(normalized.custom_js, undefined);
    assert.strictEqual(normalized.reuse_window_for_ecosystem_navigation, true);
    assert.throws(
        () => normalizeWebAppRequest(JSON.stringify({ ...app, main_url: "file:///tmp/app" })),
        /HTTP or HTTPS/,
    );

    EcosystemLauncher.setRequestHandler(null);
    EcosystemLauncher.pendingRequests.length = 0;
    assert.strictEqual(await EcosystemLauncher._receiveRequest(serialized), true);
    assert.strictEqual(EcosystemLauncher.pendingRequests.length, 1);

    const received = [];
    EcosystemLauncher.setRequestHandler((request) => received.push(request));
    await new Promise((resolve) => setImmediate(resolve));
    assert.strictEqual(received.length, 1);
    assert.strictEqual(received[0].app_name, "Apple Music");
    EcosystemLauncher.setRequestHandler(null);
}

function testElectronInstallHook() {
    const packageJson = require("../package.json");
    assert.strictEqual(
        packageJson.scripts.postinstall,
        "node node_modules/electron/install.js",
    );
}

function testAppDataPaths() {
    const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swai-data-paths-"));
    const received = {};
    try {
        const paths = configureAppDataPaths(
            { setPath: (name, value) => { received[name] = value; } },
            "apple",
            tempDir,
        );

        assert.deepStrictEqual(received, paths);
        assert.strictEqual(
            paths.userData,
            path.join(tempDir, ".local", "share", "swai", "app_data", "apple", "userData"),
        );
        assert.strictEqual(fs.statSync(paths.userData).isDirectory(), true);
        assert.strictEqual(fs.statSync(paths.sessionData).isDirectory(), true);
    } finally {
        fs.rmSync(tempDir, { recursive: true, force: true });
    }
}

async function testWidevineInitialization() {
    const componentId = "widevine-component";
    let requiredComponents;
    const installed = await ensureWidevine({
        WIDEVINE_CDM_ID: componentId,
        whenReady: async (required) => {
            requiredComponents = required;
            return [{
                id: componentId,
                status: "updated",
                title: "Widevine Content Decryption Module",
                version: "4.10.3000.0",
            }];
        },
    }, "linux");

    assert.deepStrictEqual(requiredComponents, [componentId]);
    assert.strictEqual(installed.ready, true);
    assert.strictEqual(installed.restartRequired, true);

    const alreadyInstalled = await ensureWidevine({
        WIDEVINE_CDM_ID: componentId,
        whenReady: async () => [{
            id: componentId,
            status: "new",
            version: "4.10.3000.0",
        }],
    }, "linux");
    assert.strictEqual(alreadyInstalled.restartRequired, false);

    const unsupported = await ensureWidevine(undefined, "linux");
    assert.strictEqual(unsupported.supported, false);

    const failed = await ensureWidevine({
        WIDEVINE_CDM_ID: componentId,
        whenReady: async () => { throw new Error("offline"); },
    }, "linux");
    assert.strictEqual(failed.ready, false);
    assert.strictEqual(failed.restartRequired, false);
}

async function testWidevineRelaunch() {
    const argv = [
        "/opt/swai/electron",
        "/opt/swai/index.js",
        "/tmp/Example App.swai",
        "--debug_mode=true",
    ];
    const env = {
        DISPLAY: ":0",
        PATH: "/usr/bin",
        HTTPS_PROXY: "https://proxy.example",
        LC_MESSAGES: "en_US.UTF-8",
        INVOCATION_ID: "must-not-be-copied",
    };
    const command = getWidevineRelaunchCommand({ argv, env, pid: 1234 });

    assert.strictEqual(command.file, "systemd-run");
    assert.deepStrictEqual(command.args.slice(0, 9), [
        "--user",
        "--quiet",
        "--collect",
        "--service-type=exec",
        "--expand-environment=no",
        "--same-dir",
        "--unit=swai-widevine-restart-1234",
        "--on-active=2s",
        "--timer-property=AccuracySec=100ms",
    ]);
    assert.deepStrictEqual(command.args.slice(9), [
        "--setenv=DISPLAY",
        "--setenv=HTTPS_PROXY",
        "--setenv=LC_MESSAGES",
        "--setenv=PATH",
        ...argv,
    ]);

    let execution;
    await scheduleWidevineRelaunch(
        { argv, env, pid: 1234 },
        (file, args, options, callback) => {
            execution = { file, args, options };
            callback(null, "", "");
        },
    );
    assert.strictEqual(execution.file, "systemd-run");
    assert.strictEqual(execution.options.env, env);

    await assert.rejects(scheduleWidevineRelaunch(
        { argv, env, pid: 1234 },
        (_file, _args, _options, callback) => {
            callback(new Error("failed"), "", "service unavailable");
        },
    ), {
        message: "failed: service unavailable",
    });
}

function testNavigationHandlersReceiveWindowContext() {
    const source = fs.readFileSync(
        path.join(__dirname, "../src/managers/WindowManager.js"),
        "utf8",
    );
    assert.match(
        source,
        /_setupNavigationHandlers\(mainWebContents, webapp, set_title, titlebar, currentWindowId, thisWindow\);/,
    );
    assert.match(
        source,
        /_setupNavigationHandlers\(mainWebContents, webapp, set_title, titlebar, windowId, thisWindow\)\s*\{/,
    );
}

async function main() {
    testExternalUrls();
    testManifestUrls();
    testNavigationPatterns();
    testWebAppConfiguration();
    testLinuxAppIdentity();
    testElectronInstallHook();
    testAppDataPaths();
    testNavigationHandlersReceiveWindowContext();
    await testWidevineRelaunch();
    await testWidevineInitialization();
    await testEcosystemProtocol();
    console.log("All tests passed");
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});
