// @vitest-environment node
/**
 * Integration tests for AutoUpdaterModule through the Dispatcher.
 *
 * Tests verify the full pipeline:
 * dispatcher -> Operation -> hook point -> AutoUpdaterModule handler
 */

import { createMockDispatcher } from "../intents/lib/dispatcher.test-utils";
import { describe, it, expect } from "vitest";
import { Dispatcher } from "../intents/lib/dispatcher";

import { createMinimalOperation } from "../intents/lib/operation.test-utils";
import {
  APP_START_OPERATION_ID,
  INTENT_APP_START,
  type AppStartIntent,
} from "../intents/app-start";
import {
  AppShutdownOperation,
  INTENT_APP_SHUTDOWN,
  type AppShutdownIntent,
} from "../intents/app-shutdown";
import { AppResumeOperation, INTENT_APP_RESUME, type AppResumeIntent } from "../intents/app-resume";
import { createAutoUpdaterModule } from "./auto-updater-module";
import type { AutoUpdater, UpdateDetectedCallback, DownloadProgressCallback } from "./auto-updater";
import {
  createMockConfig as createBaseMockConfig,
  type CreateMockConfigOptions,
} from "../boundaries/platform/config.test-utils";
import type { Config } from "../boundaries/platform/config";
import {
  createMockNotificationManager,
  type MockNotificationManager,
} from "./notification-manager.state-mock";

/**
 * Auto-updater tests expect `update.notification` to default to true unless
 * the test explicitly sets it. Seed it on top of the shared helper.
 */
function createMockConfig(options?: CreateMockConfigOptions): Config {
  return createBaseMockConfig({
    defaults: { "update.notification": true, ...(options?.defaults ?? {}) },
    ...(options?.overrides !== undefined && { overrides: options.overrides }),
  });
}

// =============================================================================
// Mock AutoUpdater
// =============================================================================

interface MockAutoUpdater {
  mock: AutoUpdater;
  startCalled: boolean;
  disposeCalled: boolean;
  checkForUpdatesCallCount: number;
  downloadCalled: boolean;
  quitAndInstallCalled: boolean;
  capturedDetectedCb: UpdateDetectedCallback | null;
  capturedProgressCb: DownloadProgressCallback | null;
  resolveDownload: (() => void) | null;
  rejectDownload: ((error: Error) => void) | null;
  setCheckResult: (found: boolean, version?: string) => void;
}

function createMockAutoUpdater(overrides?: {
  disposeThrows?: Error;
  checkReturns?: boolean;
}): MockAutoUpdater {
  let startCalled = false;
  let disposeCalled = false;
  let checkForUpdatesCallCount = 0;
  let downloadCalled = false;
  let quitAndInstallCalled = false;
  let capturedDetectedCb: UpdateDetectedCallback | null = null;
  let capturedProgressCb: DownloadProgressCallback | null = null;
  let resolveDownload: (() => void) | null = null;
  let rejectDownload: ((error: Error) => void) | null = null;
  let checkResult = overrides?.checkReturns ?? false;
  let detectedVersion = "2.0.0";

  const mock: AutoUpdater = {
    start() {
      startCalled = true;
    },
    async checkForUpdates() {
      checkForUpdatesCallCount++;
      if (checkResult && capturedDetectedCb) {
        capturedDetectedCb(detectedVersion);
      }
      return checkResult;
    },
    async downloadUpdate() {
      downloadCalled = true;
      return new Promise<void>((resolve, reject) => {
        resolveDownload = resolve;
        rejectDownload = reject;
      });
    },
    cancelDownload() {},
    quitAndInstall() {
      quitAndInstallCalled = true;
    },
    onUpdateDetected(callback: UpdateDetectedCallback) {
      capturedDetectedCb = callback;
      return () => {
        capturedDetectedCb = null;
      };
    },
    onUpdateDownloaded() {
      return () => {};
    },
    onDownloadProgress(callback: DownloadProgressCallback) {
      capturedProgressCb = callback;
      return () => {
        capturedProgressCb = null;
      };
    },
    dispose() {
      disposeCalled = true;
      if (overrides?.disposeThrows) {
        throw overrides.disposeThrows;
      }
    },
  } as unknown as AutoUpdater;

  return {
    mock,
    get startCalled() {
      return startCalled;
    },
    get disposeCalled() {
      return disposeCalled;
    },
    get checkForUpdatesCallCount() {
      return checkForUpdatesCallCount;
    },
    get downloadCalled() {
      return downloadCalled;
    },
    get quitAndInstallCalled() {
      return quitAndInstallCalled;
    },
    get capturedDetectedCb() {
      return capturedDetectedCb;
    },
    get capturedProgressCb() {
      return capturedProgressCb;
    },
    get resolveDownload() {
      return resolveDownload;
    },
    get rejectDownload() {
      return rejectDownload;
    },
    setCheckResult(found: boolean, version?: string) {
      checkResult = found;
      if (version) detectedVersion = version;
    },
  };
}

interface TestSetup {
  dispatcher: Dispatcher;
  autoUpdater: MockAutoUpdater;
  notificationManager: MockNotificationManager;
  mockConfig: Config;
}

function createTestSetup(overrides?: {
  disposeThrows?: Error;
  checkReturns?: boolean;
  configValues?: Record<string, unknown>;
}): TestSetup {
  const autoUpdater = createMockAutoUpdater(overrides);
  const notificationManager = createMockNotificationManager();
  const mockConfig = createMockConfig({ defaults: overrides?.configValues ?? {} });

  const dispatcher = createMockDispatcher();

  const autoUpdaterModule = createAutoUpdaterModule({
    autoUpdater: autoUpdater.mock,
    dispatcher,
    configService: mockConfig,
    notificationManager: notificationManager.manager,
  });

  dispatcher.registerOperation(
    INTENT_APP_START,
    createMinimalOperation(APP_START_OPERATION_ID, "start")
  );
  dispatcher.registerOperation(INTENT_APP_SHUTDOWN, new AppShutdownOperation());
  dispatcher.registerOperation(INTENT_APP_RESUME, new AppResumeOperation());

  dispatcher.registerModule(autoUpdaterModule);

  return { dispatcher, autoUpdater, notificationManager, mockConfig };
}

function startIntent(): AppStartIntent {
  return { type: INTENT_APP_START, payload: {} as AppStartIntent["payload"] };
}

function shutdownIntent(installUpdate?: boolean): AppShutdownIntent {
  return {
    type: INTENT_APP_SHUTDOWN,
    payload: { ...(installUpdate !== undefined && { installUpdate }) },
  };
}

function resumeIntent(): AppResumeIntent {
  return { type: INTENT_APP_RESUME, payload: {} as AppResumeIntent["payload"] };
}

// Tiny helper to flush microtasks (and the void runCheck() chain).
async function flush(): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, 0));
  await new Promise((resolve) => setTimeout(resolve, 0));
}

// =============================================================================
// Tests
// =============================================================================

describe("AutoUpdaterModule Integration", () => {
  it("app:start calls autoUpdater.start() and runs initial check", async () => {
    const { dispatcher, autoUpdater } = createTestSetup();

    await dispatcher.dispatch(startIntent());
    await flush();

    expect(autoUpdater.startCalled).toBe(true);
    expect(autoUpdater.checkForUpdatesCallCount).toBe(1);
  });

  it("app:start with update.notification=false skips check and timer", async () => {
    const { dispatcher, autoUpdater, notificationManager } = createTestSetup({
      configValues: { "update.notification": false },
      checkReturns: true,
    });

    await dispatcher.dispatch(startIntent());
    await flush();

    expect(autoUpdater.checkForUpdatesCallCount).toBe(0);
    expect(notificationManager.notifications).toHaveLength(0);
  });

  it("startup check finding an update opens 'Update available' notification", async () => {
    const { dispatcher, notificationManager } = createTestSetup({ checkReturns: true });

    await dispatcher.dispatch(startIntent());
    await flush();

    expect(notificationManager.notifications).toHaveLength(1);
    const cfg = notificationManager.notifications[0]!.opened;
    expect(cfg.type).toBe("info");
    expect(cfg.title).toBe("Update available");
    expect(cfg.message).toContain("2.0.0");
    expect(cfg.dismissible).toBe(true);
    expect(cfg.actions).toEqual([{ id: "install", label: "Install" }]);
  });

  it("clicking Install triggers download and surfaces 'Update ready' on completion", async () => {
    const { dispatcher, autoUpdater, notificationManager } = createTestSetup({
      checkReturns: true,
    });
    await dispatcher.dispatch(startIntent());
    await flush();

    notificationManager.emitEvent(0, { actionId: "install" });
    await flush();

    expect(autoUpdater.downloadCalled).toBe(true);
    // Notification updated to downloading state
    const slot = notificationManager.notifications[0]!;
    expect(slot.updates[0]!.title).toBe("Downloading update");

    // Download completes
    autoUpdater.resolveDownload!();
    await flush();

    const last = slot.updates[slot.updates.length - 1]!;
    expect(last.title).toBe("Update ready");
    expect(last.actions).toEqual([{ id: "restart", label: "Restart Now" }]);
    expect(last.dismissible).toBe(false);
  });

  it("download progress events update the notification with percent", async () => {
    const { dispatcher, autoUpdater, notificationManager } = createTestSetup({
      checkReturns: true,
    });
    await dispatcher.dispatch(startIntent());
    await flush();

    notificationManager.emitEvent(0, { actionId: "install" });
    await flush();

    autoUpdater.capturedProgressCb!({ percent: 50 });
    const slot = notificationManager.notifications[0]!;
    const progressUpdate = slot.updates.find((u) => u.progress === 0.5);
    expect(progressUpdate).toBeDefined();
    expect(progressUpdate!.title).toBe("Downloading update");
  });

  it("download failure swaps to 'Update failed' notification with retry", async () => {
    const { dispatcher, autoUpdater, notificationManager } = createTestSetup({
      checkReturns: true,
    });
    await dispatcher.dispatch(startIntent());
    await flush();

    notificationManager.emitEvent(0, { actionId: "install" });
    await flush();

    autoUpdater.rejectDownload!(new Error("network down"));
    await flush();

    const slot = notificationManager.notifications[0]!;
    const last = slot.updates[slot.updates.length - 1]!;
    expect(last.type).toBe("error");
    expect(last.title).toBe("Update failed");
    expect(last.actions).toEqual([{ id: "retry", label: "Retry" }]);
  });

  it("clicking Restart dispatches app:shutdown with installUpdate", async () => {
    const { dispatcher, autoUpdater, notificationManager } = createTestSetup({
      checkReturns: true,
    });
    await dispatcher.dispatch(startIntent());
    await flush();

    notificationManager.emitEvent(0, { actionId: "install" });
    await flush();
    autoUpdater.resolveDownload!();
    await flush();

    notificationManager.emitEvent(0, { actionId: "restart" });
    await flush();

    expect(autoUpdater.quitAndInstallCalled).toBe(true);
  });

  it("app:resume runs an update check", async () => {
    const { dispatcher, autoUpdater } = createTestSetup({ checkReturns: false });
    await dispatcher.dispatch(startIntent());
    await flush();
    const startupCount = autoUpdater.checkForUpdatesCallCount;

    await dispatcher.dispatch(resumeIntent());

    expect(autoUpdater.checkForUpdatesCallCount).toBe(startupCount + 1);
  });

  it("app:resume with update.notification=false does not check", async () => {
    const { dispatcher, autoUpdater } = createTestSetup({
      configValues: { "update.notification": false },
    });
    await dispatcher.dispatch(startIntent());

    await dispatcher.dispatch(resumeIntent());

    expect(autoUpdater.checkForUpdatesCallCount).toBe(0);
  });

  it("second check after a version was surfaced does not open another notification", async () => {
    const { dispatcher, notificationManager } = createTestSetup({ checkReturns: true });
    await dispatcher.dispatch(startIntent());
    await flush();
    expect(notificationManager.notifications).toHaveLength(1);

    await dispatcher.dispatch(resumeIntent());
    await flush();

    expect(notificationManager.notifications).toHaveLength(1);
  });

  it("app:shutdown calls autoUpdater.dispose()", async () => {
    const { dispatcher, autoUpdater } = createTestSetup();

    await dispatcher.dispatch(shutdownIntent());

    expect(autoUpdater.disposeCalled).toBe(true);
  });

  it("dispose() throws — collect catches error, dispatch still resolves", async () => {
    const { dispatcher } = createTestSetup({
      disposeThrows: new Error("dispose failed"),
    });

    await expect(dispatcher.dispatch(shutdownIntent())).resolves.toBeUndefined();
  });

  it("quit hook calls quitAndInstall when installUpdate is set", async () => {
    const { dispatcher, autoUpdater } = createTestSetup();

    await dispatcher.dispatch(shutdownIntent(true));

    expect(autoUpdater.quitAndInstallCalled).toBe(true);
  });

  it("quit hook does NOT call quitAndInstall when installUpdate is not set", async () => {
    const { dispatcher, autoUpdater } = createTestSetup();

    await dispatcher.dispatch(shutdownIntent());

    expect(autoUpdater.quitAndInstallCalled).toBe(false);
  });
});
