import { describe, expect, it, vi } from "vitest";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { defaultLogger, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
import {
    CENTRAL_COMPATIBILITY_REJECTION_REASONS,
    NO_INTERACTION,
    REPLICATION_PROGRESS_PRESENTATIONS,
    USER_INITIATED_REPLICATION_AUTHORITY,
    replicationFailed,
} from "@vrtmrz/livesync-commonlib/replication";

const chunkMocks = vi.hoisted(() => ({
    purgeUnreferencedChunks: vi.fn(async (_database: unknown, countOnly: boolean) => (countOnly ? 2 : 0)),
    balanceChunkPurgedDBs: vi.fn(async () => undefined),
}));

vi.mock("@vrtmrz/livesync-commonlib/compat/pouchdb/chunks", () => chunkMocks);
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
    LiveSyncCouchDBReplicator: class {},
}));

import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
import { createCentralCompatibilityRecovery } from "./centralCompatibilityRecovery";

describe("central compatibility recovery", () => {
    it("characterises unattended central failure handling as one INFO log without a NOTICE", async () => {
        const log = vi.fn((_message: unknown, _level?: number, _key?: string) => undefined);
        setGlobalLogFunction(log);
        try {
            const recovery = createCentralCompatibilityRecovery({
                confirm: {},
                getLocalDatabase: () => ({}),
                rebuilder: {},
                services: {
                    appLifecycle: {},
                    API: {},
                    replicator: {},
                    tweakValue: {},
                },
            } as never);

            await expect(
                recovery.handleReplicationFailure({
                    context: { provider: {}, replicator: {} },
                    setting: {},
                    outcome: replicationFailed(new Error("provider failed")),
                    progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET,
                    interaction: NO_INTERACTION,
                } as never)
            ).resolves.toBe(false);

            expect(log).toHaveBeenCalledOnce();
            expect(log).toHaveBeenCalledWith("Replication failed on an unattended path.", LOG_LEVEL_INFO, undefined);
            expect(log.mock.calls.map(([, level]) => level)).not.toContain(LOG_LEVEL_NOTICE);
        } finally {
            setGlobalLogFunction(defaultLogger);
        }
    });

    it("uses the exact failed outcome and permits dialogue only with recovery authority", async () => {
        const askResolvingMismatched = vi.fn(async (..._arguments: unknown[]) => undefined);
        const failedSetPreferred = vi.fn(async (_setting: unknown) => undefined);
        const failedReplicator = { setPreferredRemoteTweakSettings: failedSetPreferred };
        const replacementSetPreferred = vi.fn(async (_setting: unknown) => undefined);
        const replacementReplicator = {
            tweakSettingsMismatched: true,
            preferredTweakValue: { customChunkSize: 99 },
            setPreferredRemoteTweakSettings: replacementSetPreferred,
        };
        const failedContext = { provider: {}, replicator: failedReplicator };
        const replacementContext = { provider: {}, replicator: replacementReplicator };
        const preferredTweakValue = { customChunkSize: 60 };
        const outcome = replicationFailed(new Error("mismatched"), {
            reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH,
            preferredTweakValue,
        });
        const recovery = createCentralCompatibilityRecovery({
            confirm: {},
            getLocalDatabase: () => ({}),
            rebuilder: {},
            services: {
                appLifecycle: {},
                API: {},
                replicator: {
                    runWithActiveReplicatorContext: vi.fn(async (task: (context: unknown) => unknown) =>
                        task(replacementContext)
                    ),
                },
                tweakValue: { askResolvingMismatched },
            },
        } as never);

        await recovery.handleReplicationFailure({
            context: failedContext,
            setting: {},
            outcome,
            progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET,
            interaction: NO_INTERACTION,
        } as never);
        expect(askResolvingMismatched).not.toHaveBeenCalled();

        await recovery.handleReplicationFailure({
            context: failedContext,
            setting: {},
            outcome,
            progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET,
            interaction: {
                kind: "permitted",
                permissions: { ...USER_INITIATED_REPLICATION_AUTHORITY.permissions, failureRecovery: false },
            },
        } as never);
        expect(askResolvingMismatched).not.toHaveBeenCalled();

        await recovery.handleReplicationFailure({
            context: failedContext,
            setting: {},
            outcome,
            progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET,
            interaction: USER_INITIATED_REPLICATION_AUTHORITY,
        } as never);
        expect(askResolvingMismatched).toHaveBeenCalledWith(preferredTweakValue, expect.any(Function));
        const updatePreferredRemote = askResolvingMismatched.mock.calls[0][1] as (
            setting: Record<string, unknown>
        ) => Promise<boolean>;
        await expect(updatePreferredRemote({ customChunkSize: 64 })).resolves.toBe(false);
        expect(failedSetPreferred).not.toHaveBeenCalled();
        expect(replacementSetPreferred).not.toHaveBeenCalled();
    });

    it("writes a mismatch decision only through the still-active failed publication", async () => {
        const setPreferredRemoteTweakSettings = vi.fn(async (_setting: unknown) => undefined);
        const failedContext = { provider: {}, replicator: { setPreferredRemoteTweakSettings } };
        let updatePreferredRemote: ((setting: Record<string, unknown>) => Promise<boolean>) | undefined;
        const askResolvingMismatched = vi.fn(
            async (_preferred: unknown, update: (setting: Record<string, unknown>) => Promise<boolean>) => {
                updatePreferredRemote = update;
            }
        );
        const recovery = createCentralCompatibilityRecovery({
            confirm: {},
            getLocalDatabase: () => ({}),
            rebuilder: {},
            services: {
                appLifecycle: {},
                API: {},
                replicator: {
                    runWithActiveReplicatorContext: vi.fn(async (task: (context: unknown) => unknown) =>
                        task(failedContext)
                    ),
                },
                tweakValue: { askResolvingMismatched },
            },
        } as never);

        await recovery.handleReplicationFailure({
            context: failedContext,
            setting: {},
            outcome: replicationFailed(new Error("mismatched"), {
                reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH,
                preferredTweakValue: { customChunkSize: 60 },
            }),
            progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
            interaction: USER_INITIATED_REPLICATION_AUTHORITY,
        } as never);

        const effectiveSetting = { customChunkSize: 64 };
        await expect(updatePreferredRemote?.(effectiveSetting)).resolves.toBe(true);
        expect(setPreferredRemoteTweakSettings).toHaveBeenCalledWith(effectiveSetting);
        expect(setPreferredRemoteTweakSettings.mock.calls[0][0]).not.toBe(effectiveSetting);
    });

    it("does not apply an unlock selected for a replaced failed publication", async () => {
        const failedMarkResolved = vi.fn(async () => undefined);
        const replacementMarkResolved = vi.fn(async () => undefined);
        const failedContext = { provider: {}, replicator: { markRemoteResolved: failedMarkResolved } };
        const replacementContext = { provider: {}, replicator: { markRemoteResolved: replacementMarkResolved } };
        const runWithActiveReplicatorContext = vi.fn(async (task: (context: unknown) => unknown) =>
            task(replacementContext)
        );
        const recovery = createCentralCompatibilityRecovery({
            confirm: {
                askSelectStringDialogue: vi.fn(async (_message: string, choices: string[]) => choices[1]),
            },
            getLocalDatabase: () => ({}),
            rebuilder: {},
            services: {
                appLifecycle: { scheduleRestart: vi.fn() },
                API: {},
                replicator: { runWithActiveReplicatorContext },
                tweakValue: {},
            },
        } as never);

        await recovery.handleReplicationFailure({
            context: failedContext,
            setting: {},
            outcome: replicationFailed(new Error("locked"), {
                reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_LOCKED,
            }),
            progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
            interaction: USER_INITIATED_REPLICATION_AUTHORITY,
        } as never);

        expect(runWithActiveReplicatorContext).toHaveBeenCalledOnce();
        expect(failedMarkResolved).not.toHaveBeenCalled();
        expect(replacementMarkResolved).not.toHaveBeenCalled();
    });

    it("keeps cleaned-remote replication and balancing inside the shared activity boundary", async () => {
        const activityFinished = vi.fn();
        const runBoundedRemoteActivity = vi.fn(async (task: () => unknown) => {
            try {
                return await task();
            } finally {
                activityFinished();
            }
        });
        const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task());
        const openOneShotReplication = vi.fn(async () => true);
        const remoteDatabase = { close: vi.fn(async () => undefined) };
        const close = vi.fn(async () => undefined);
        const activeReplicator = Object.assign(new LiveSyncCouchDBReplicator({} as never), {
            connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: remoteDatabase, close })),
            openOneShotReplication,
            markRemoteResolved: vi.fn(async () => undefined),
        });
        const expectedContext = { provider: {}, replicator: activeReplicator };
        const runWithActiveReplicatorContext = vi.fn(async (task: (context: unknown) => unknown) =>
            task(expectedContext)
        );
        const localDatabase = { localDatabase: {}, clearCaches: vi.fn() };
        const getLocalDatabase = vi.fn(() => localDatabase);
        const recovery = createCentralCompatibilityRecovery({
            confirm: { confirmWithMessage: vi.fn(async () => "Cleanup") },
            getLocalDatabase,
            rebuilder: {},
            services: {
                appLifecycle: {},
                API: { isMobile: vi.fn(() => false) },
                replicator: {
                    runBoundedRemoteActivity,
                    runFiniteReplicationActivity,
                    runWithActiveReplicatorContext,
                },
                tweakValue: {},
            },
        } as never);

        await recovery.reconcileCleanedRemote(true, {} as ObsidianLiveSyncSettings, expectedContext as never);

        expect(runBoundedRemoteActivity).toHaveBeenCalledWith(expect.any(Function), {
            label: "database-cleanup",
        });
        expect(runFiniteReplicationActivity).toHaveBeenCalledWith(expect.any(Function), {
            label: "replication",
        });
        expect(runWithActiveReplicatorContext).toHaveBeenCalledOnce();
        expect(openOneShotReplication).toHaveBeenCalledOnce();
        expect(openOneShotReplication.mock.invocationCallOrder[0]).toBeLessThan(
            activityFinished.mock.invocationCallOrder[0]
        );
        expect(chunkMocks.balanceChunkPurgedDBs).toHaveBeenCalledOnce();
        expect(getLocalDatabase).toHaveBeenCalledTimes(2);
        expect(close).toHaveBeenCalledOnce();
        expect(close.mock.invocationCallOrder[0]).toBeLessThan(activityFinished.mock.invocationCallOrder[0]);
    });
});
