import * as processSetting from "@lib/API/processSetting";
import { ConnectionStringParser } from "@lib/common/ConnectionString";
import { configURIBase } from "@lib/common/models/shared.const";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P } from "@lib/common/types";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { runCommand } from "./runCommand";
import type { CLIOptions } from "./types";
import * as commandUtils from "./utils";

function createCoreMock() {
    const liveSettings = {
        ...DEFAULT_SETTINGS,
        remoteConfigurations: {},
        activeConfigurationId: "",
        P2P_ActiveRemoteConfigurationId: "",
    } as any;
    return {
        services: {
            control: {
                activated: Promise.resolve(),
                applySettings: vi.fn(async () => {}),
            },
            setting: {
                applyExternalSettings: vi.fn(async () => {}),
                applyPartial: vi.fn(async () => {}),
                currentSettings: vi.fn(() => liveSettings),
                updateSettings: vi.fn(async (updater: any) => {
                    updater(liveSettings);
                }),
            },
        },
        serviceModules: {
            fileHandler: {
                dbToStorage: vi.fn(async () => true),
                storeFileToDB: vi.fn(async () => true),
            },
            storageAccess: {
                readFileAuto: vi.fn(async () => ""),
                writeFileAuto: vi.fn(async () => {}),
            },
            databaseFileAccess: {
                fetch: vi.fn(async () => undefined),
            },
        },
    } as any;
}

function makeOptions(command: CLIOptions["command"], commandArgs: string[]): CLIOptions {
    return {
        command,
        commandArgs,
        databasePath: "/tmp/vault",
        verbose: false,
        force: false,
    };
}

async function createSetupURI(passphrase: string): Promise<string> {
    const settings = {
        ...DEFAULT_SETTINGS,
        couchDB_URI: "http://127.0.0.1:5984",
        couchDB_DBNAME: "livesync-test-db",
        couchDB_USER: "user",
        couchDB_PASSWORD: "pass",
        isConfigured: true,
    } as any;
    return await processSetting.encodeSettingsToSetupURI(settings, passphrase);
}

function captureStdout() {
    const writes: string[] = [];
    const spy = vi.spyOn(process.stdout, "write").mockImplementation((chunk: any) => {
        writes.push(typeof chunk === "string" ? chunk : String(chunk));
        return true;
    });
    return {
        spy,
        lines: () =>
            writes
                .join("")
                .split("\n")
                .map((e) => e.trim())
                .filter((e) => e.length > 0),
    };
}

function parseAddedRemoteIdFromLines(lines: string[]): string {
    // remote-add prints: <id>\t<name>\t<redacted-connstr>
    const last = lines.length > 0 ? lines[lines.length - 1] : "";
    return last.split("\t")[0] || "";
}

type ProtocolFixture = {
    protocol: string;
    connectionString: string;
    assertProjectedFields: (settings: any) => void;
};

const protocolFixtures: ProtocolFixture[] = [
    {
        protocol: "couchdb",
        connectionString: ConnectionStringParser.serialize({
            type: "couchdb",
            settings: {
                couchDB_URI: "https://db.example.com:5984",
                couchDB_USER: "user1",
                couchDB_PASSWORD: "pass1",
                couchDB_DBNAME: "vault1",
                couchDB_CustomHeaders: "",
                useJWT: false,
                jwtAlgorithm: "",
                jwtKey: "",
                jwtKid: "",
                jwtSub: "",
                jwtExpDuration: 5,
                useRequestAPI: false,
            },
        }),
        assertProjectedFields: (settings) => {
            expect(settings.remoteType).toBe(REMOTE_COUCHDB);
            expect(settings.couchDB_URI).toBe("https://db.example.com:5984");
            expect(settings.couchDB_USER).toBe("user1");
            expect(settings.couchDB_PASSWORD).toBe("pass1");
            expect(settings.couchDB_DBNAME).toBe("vault1");
        },
    },
    {
        protocol: "s3",
        connectionString: ConnectionStringParser.serialize({
            type: "s3",
            settings: {
                accessKey: "ak",
                secretKey: "sk",
                endpoint: "https://s3.example.com",
                bucket: "bucket-1",
                region: "ap-northeast-1",
                bucketPrefix: "vault/",
                useCustomRequestHandler: true,
                bucketCustomHeaders: "x-test:1",
                forcePathStyle: false,
            },
        }),
        assertProjectedFields: (settings) => {
            expect(settings.remoteType).toBe(REMOTE_MINIO);
            expect(settings.accessKey).toBe("ak");
            expect(settings.secretKey).toBe("sk");
            expect(settings.endpoint).toBe("https://s3.example.com");
            expect(settings.bucket).toBe("bucket-1");
            expect(settings.region).toBe("ap-northeast-1");
        },
    },
    {
        protocol: "p2p",
        connectionString: ConnectionStringParser.serialize({
            type: "p2p",
            settings: {
                P2P_Enabled: false,
                P2P_roomID: "room-abc",
                P2P_passphrase: "pass-123",
                P2P_relays: "wss://relay.example",
                P2P_AppID: "self-hosted-livesync",
                P2P_AutoStart: true,
                P2P_AutoBroadcast: false,
                P2P_turnServers: "turn:turn.example:3478",
                P2P_turnUsername: "turn-user",
                P2P_turnCredential: "turn-pass",
            },
        }),
        assertProjectedFields: (settings) => {
            expect(settings.remoteType).toBe(REMOTE_P2P);
            expect(settings.P2P_roomID).toBe("room-abc");
            expect(settings.P2P_passphrase).toBe("pass-123");
            expect(settings.P2P_relays).toBe("wss://relay.example");
            expect(settings.P2P_AppID).toBe("self-hosted-livesync");
        },
    },
];

describe("runCommand abnormal cases", () => {
    const context = {
        databasePath: "/tmp/vault",
        settingsPath: "/tmp/vault/.livesync/settings.json",
    } as any;

    beforeEach(() => {
        vi.restoreAllMocks();
    });

    afterEach(() => {
        vi.restoreAllMocks();
    });

    it("pull returns false for non-existing path", async () => {
        const core = createCoreMock();
        core.serviceModules.fileHandler.dbToStorage.mockResolvedValue(false);

        const result = await runCommand(makeOptions("pull", ["missing.md", "/tmp/out.md"]), {
            ...context,
            core,
        });

        expect(result).toBe(false);
        expect(core.serviceModules.fileHandler.dbToStorage).toHaveBeenCalled();
    });

    it("pull-rev throws on empty revision", async () => {
        const core = createCoreMock();

        await expect(
            runCommand(makeOptions("pull-rev", ["file.md", "/tmp/out.md", "   "]), {
                ...context,
                core,
            })
        ).rejects.toThrow("pull-rev requires a non-empty revision");
    });

    it("pull-rev returns false for invalid revision", async () => {
        const core = createCoreMock();
        core.serviceModules.databaseFileAccess.fetch.mockResolvedValue(undefined);

        const result = await runCommand(makeOptions("pull-rev", ["file.md", "/tmp/out.md", "9-invalid"]), {
            ...context,
            core,
        });

        expect(result).toBe(false);
        expect(core.serviceModules.databaseFileAccess.fetch).toHaveBeenCalledWith("file.md", "9-invalid", true);
    });

    it("cat-rev throws on empty revision", async () => {
        const core = createCoreMock();

        await expect(
            runCommand(makeOptions("cat-rev", ["file.md", "   "]), {
                ...context,
                core,
            })
        ).rejects.toThrow("cat-rev requires a non-empty revision");
    });

    it("cat-rev returns false for invalid revision", async () => {
        const core = createCoreMock();
        core.serviceModules.databaseFileAccess.fetch.mockResolvedValue(undefined);

        const result = await runCommand(makeOptions("cat-rev", ["file.md", "9-invalid"]), {
            ...context,
            core,
        });

        expect(result).toBe(false);
        expect(core.serviceModules.databaseFileAccess.fetch).toHaveBeenCalledWith("file.md", "9-invalid", true);
    });

    it("push rejects when source file does not exist", async () => {
        const core = createCoreMock();

        await expect(
            runCommand(makeOptions("push", ["/tmp/livesync-missing-src-file.md", "dst.md"]), {
                ...context,
                core,
            })
        ).rejects.toMatchObject({ code: "ENOENT" });
    });

    it("setup rejects invalid URI", async () => {
        const core = createCoreMock();

        await expect(
            runCommand(makeOptions("setup", ["https://invalid.example/setup"]), {
                ...context,
                core,
            })
        ).rejects.toThrow(`setup URI must start with ${configURIBase}`);
    });

    it("setup rejects empty passphrase", async () => {
        const core = createCoreMock();
        vi.spyOn(commandUtils, "promptForPassphrase").mockRejectedValue(new Error("Passphrase is required"));

        await expect(
            runCommand(makeOptions("setup", [`${configURIBase}dummy`]), {
                ...context,
                core,
            })
        ).rejects.toThrow("Passphrase is required");
    });

    it("setup accepts URI generated by encodeSettingsToSetupURI", async () => {
        const core = createCoreMock();
        const passphrase = "correct-passphrase";
        const setupURI = await createSetupURI(passphrase);
        vi.spyOn(commandUtils, "promptForPassphrase").mockResolvedValue(passphrase);

        const result = await runCommand(makeOptions("setup", [setupURI]), {
            ...context,
            core,
        });

        expect(result).toBe(true);
        expect(core.services.setting.applyExternalSettings).toHaveBeenCalledTimes(1);
        expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
        const [appliedSettings, saveImmediately] = core.services.setting.applyExternalSettings.mock.calls[0];
        expect(saveImmediately).toBe(true);
        expect(appliedSettings.couchDB_URI).toBe("http://127.0.0.1:5984");
        expect(appliedSettings.couchDB_DBNAME).toBe("livesync-test-db");
        expect(appliedSettings.isConfigured).toBe(true);
        expect(appliedSettings.useIndexedDBAdapter).toBe(false);
    });

    it("setup rejects encoded URI when passphrase is wrong", async () => {
        const core = createCoreMock();
        const setupURI = await createSetupURI("correct-passphrase");
        vi.spyOn(commandUtils, "promptForPassphrase").mockResolvedValue("wrong-passphrase");

        await expect(
            runCommand(makeOptions("setup", [setupURI]), {
                ...context,
                core,
            })
        ).rejects.toThrow();

        expect(core.services.setting.applyExternalSettings).not.toHaveBeenCalled();
        expect(core.services.control.applySettings).not.toHaveBeenCalled();
    });

    it("remote-add stores canonical URI and prints the created id", async () => {
        const core = createCoreMock();
        const stdout = vi.spyOn(process.stdout, "write").mockImplementation(() => true);

        const result = await runCommand(makeOptions("remote-add", ["my-remote", "sls+https://example.com/db"]), {
            ...context,
            core,
        });

        expect(result).toBe(true);
        const settings = core.services.setting.currentSettings();
        const ids = Object.keys(settings.remoteConfigurations);
        expect(ids.length).toBe(1);
        expect(settings.remoteConfigurations[ids[0]].name).toBe("my-remote");
        expect(settings.remoteConfigurations[ids[0]].uri).toContain("sls+https://example.com/db");
        expect(settings.activeConfigurationId).toBe(ids[0]);
        expect(stdout).toHaveBeenCalled();
    });

    it("remote-activate switches active remote and applies settings", async () => {
        const core = createCoreMock();
        const settings = core.services.setting.currentSettings();
        settings.remoteConfigurations.r1 = {
            id: "r1",
            name: "R1",
            uri: "sls+https://example.com/db1",
            isEncrypted: false,
        };
        settings.remoteConfigurations.r2 = {
            id: "r2",
            name: "R2",
            uri: "sls+https://example.com/db2",
            isEncrypted: false,
        };
        settings.activeConfigurationId = "r1";

        const result = await runCommand(makeOptions("remote-activate", ["r2"]), {
            ...context,
            core,
        });

        expect(result).toBe(true);
        expect(settings.activeConfigurationId).toBe("r2");
        expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
    });

    it("remote-rm removes active remote and promotes first remaining", async () => {
        const core = createCoreMock();
        const settings = core.services.setting.currentSettings();
        settings.remoteConfigurations.r1 = {
            id: "r1",
            name: "R1",
            uri: "sls+https://example.com/db1",
            isEncrypted: false,
        };
        settings.remoteConfigurations.r2 = {
            id: "r2",
            name: "R2",
            uri: "sls+https://example.com/db2",
            isEncrypted: false,
        };
        settings.activeConfigurationId = "r1";

        const result = await runCommand(makeOptions("remote-rm", ["r1"]), {
            ...context,
            core,
        });

        expect(result).toBe(true);
        expect(settings.remoteConfigurations.r1).toBeUndefined();
        expect(settings.activeConfigurationId).toBe("r2");
        expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
    });

    it("remote-export prints the exact stored connection string", async () => {
        const core = createCoreMock();
        const settings = core.services.setting.currentSettings();
        settings.remoteConfigurations.r1 = {
            id: "r1",
            name: "R1",
            uri: "sls+https://example.com/db?db=vault",
            isEncrypted: false,
        };
        const stdout = captureStdout();

        const result = await runCommand(makeOptions("remote-export", ["r1"]), {
            ...context,
            core,
        });

        expect(result).toBe(true);
        const outLines = stdout.lines();
        expect(outLines.length > 0 ? outLines[outLines.length - 1] : "").toBe("sls+https://example.com/db?db=vault");
        expect(stdout.spy).toHaveBeenCalled();
    });

    it("remote-set updates URI and applies settings when target is active", async () => {
        const core = createCoreMock();
        const settings = core.services.setting.currentSettings();
        settings.remoteConfigurations.r1 = {
            id: "r1",
            name: "R1",
            uri: "sls+https://old.example/db",
            isEncrypted: false,
        };
        settings.activeConfigurationId = "r1";

        const result = await runCommand(makeOptions("remote-set", ["r1", "sls+https://new.example/db"]), {
            ...context,
            core,
        });

        expect(result).toBe(true);
        expect(settings.remoteConfigurations.r1.uri).toContain("sls+https://new.example/db");
        expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
    });

    it.each(protocolFixtures)(
        "remote-activate projects effective settings for $protocol",
        async ({ connectionString, assertProjectedFields }) => {
            const core = createCoreMock();
            const settings = core.services.setting.currentSettings();
            settings.remoteConfigurations.r1 = {
                id: "r1",
                name: "R1",
                uri: "sls+https://old.example/?db=old",
                isEncrypted: false,
            };
            settings.remoteConfigurations.r2 = {
                id: "r2",
                name: "R2",
                uri: connectionString,
                isEncrypted: false,
            };
            settings.activeConfigurationId = "r1";

            const result = await runCommand(makeOptions("remote-activate", ["r2"]), {
                ...context,
                core,
            });

            expect(result).toBe(true);
            expect(settings.activeConfigurationId).toBe("r2");
            assertProjectedFields(settings);
        }
    );

    it.each(protocolFixtures)(
        "remote-set projects effective settings for active remote ($protocol)",
        async ({ connectionString, assertProjectedFields }) => {
            const core = createCoreMock();
            const settings = core.services.setting.currentSettings();
            settings.remoteConfigurations.r1 = {
                id: "r1",
                name: "R1",
                uri: "sls+https://old.example/?db=old",
                isEncrypted: false,
            };
            settings.activeConfigurationId = "r1";

            const result = await runCommand(makeOptions("remote-set", ["r1", connectionString]), {
                ...context,
                core,
            });

            expect(result).toBe(true);
            assertProjectedFields(settings);
        }
    );

    it.each(protocolFixtures)(
        "remote-rm projects promoted active remote effective settings for $protocol",
        async ({ connectionString, assertProjectedFields }) => {
            const core = createCoreMock();
            const settings = core.services.setting.currentSettings();
            settings.remoteConfigurations.r1 = {
                id: "r1",
                name: "R1",
                uri: "sls+https://old.example/?db=old",
                isEncrypted: false,
            };
            settings.remoteConfigurations.r2 = {
                id: "r2",
                name: "R2",
                uri: connectionString,
                isEncrypted: false,
            };
            settings.activeConfigurationId = "r1";

            const result = await runCommand(makeOptions("remote-rm", ["r1"]), {
                ...context,
                core,
            });

            expect(result).toBe(true);
            expect(settings.activeConfigurationId).toBe("r2");
            assertProjectedFields(settings);
        }
    );

    it.each([
        ["couchdb", "sls+https://user:pass@example.com:5984/?db=vault"] as const,
        [
            "s3",
            "sls+s3://ak:sk@example.com/?endpoint=https%3A%2F%2Fs3.example.com&bucket=my-bucket&region=ap-northeast-1",
        ] as const,
        [
            "p2p",
            "sls+p2p://room-abc?passphrase=pass-123&relays=wss%3A%2F%2Frelay.example&appId=self-hosted-livesync",
        ] as const,
    ])("remote command round-trip works for %s", async (_protocol, initialConnStr) => {
        const core = createCoreMock();

        const addOut = captureStdout();
        const addResult = await runCommand(makeOptions("remote-add", ["rt", initialConnStr]), {
            ...context,
            core,
        });
        expect(addResult).toBe(true);
        const remoteId = parseAddedRemoteIdFromLines(addOut.lines());
        expect(remoteId).not.toBe("");

        const export1Out = captureStdout();
        const export1Result = await runCommand(makeOptions("remote-export", [remoteId]), {
            ...context,
            core,
        });
        expect(export1Result).toBe(true);
        const export1Lines = export1Out.lines();
        const exported1 = export1Lines.length > 0 ? export1Lines[export1Lines.length - 1] : "";
        expect(exported1).toBe(ConnectionStringParser.serialize(ConnectionStringParser.parse(initialConnStr)));

        const roundTripInput = ConnectionStringParser.serialize(ConnectionStringParser.parse(exported1));
        const setResult = await runCommand(makeOptions("remote-set", [remoteId, roundTripInput]), {
            ...context,
            core,
        });
        expect(setResult).toBe(true);

        const export2Out = captureStdout();
        const export2Result = await runCommand(makeOptions("remote-export", [remoteId]), {
            ...context,
            core,
        });
        expect(export2Result).toBe(true);
        const export2Lines = export2Out.lines();
        const exported2 = export2Lines.length > 0 ? export2Lines[export2Lines.length - 1] : "";
        expect(exported2).toBe(roundTripInput);
    });
});
