import test from "node:test";
import assert from "node:assert/strict";

import type { SyncPayload } from "../domain/sync.ts";
import type { KnownHost } from "../domain/models.ts";
import type { SyncableVaultData } from "./syncPayload.ts";

type LocalStorageMock = {
  clear(): void;
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
  removeItem(key: string): void;
};

function installLocalStorage(): LocalStorageMock {
  const store = new Map<string, string>();
  const localStorage: LocalStorageMock = {
    clear() {
      store.clear();
    },
    getItem(key: string) {
      return store.has(key) ? store.get(key)! : null;
    },
    setItem(key: string, value: string) {
      store.set(key, String(value));
    },
    removeItem(key: string) {
      store.delete(key);
    },
  };
  Object.defineProperty(globalThis, "localStorage", {
    value: localStorage,
    configurable: true,
  });
  return localStorage;
}

const localStorage = installLocalStorage();
const {
  applyLocalVaultPayload,
  applySyncPayload,
  buildLocalVaultPayload,
  buildSyncPayload,
  hasCloudSyncEntityData,
  hasMeaningfulCloudSyncData,
  shouldPromptCloudVaultRecovery,
  SYNCABLE_SETTING_STORAGE_KEYS,
} = await import("./syncPayload.ts");
const storageKeys = await import("../infrastructure/config/storageKeys.ts");

const knownHost = (id = "kh-1"): KnownHost => ({
  id,
  hostname: `${id}.example.com`,
  port: 22,
  keyType: "ssh-ed25519",
  publicKey: `SHA256:${id}`,
  discoveredAt: 1,
});

const vault = (knownHosts: KnownHost[] = [knownHost()]): SyncableVaultData => ({
  hosts: [],
  keys: [],
  identities: [],
  snippets: [],
  customGroups: [],
  snippetPackages: [],
  knownHosts,
  groupConfigs: [],
});

test.beforeEach(() => {
  localStorage.clear();
});

test("buildSyncPayload treats known hosts as local-only data", () => {
  const payload = buildSyncPayload(vault([knownHost("kh-cloud")]));

  assert.equal("knownHosts" in payload, false);
});

test("buildSyncPayload includes reusable proxy profiles", () => {
  const proxyProfiles = [
    {
      id: "proxy-1",
      label: "Office Proxy",
      config: { type: "socks5", host: "proxy.example.com", port: 1080 },
      createdAt: 1,
      updatedAt: 1,
    },
  ];

  const payload = buildSyncPayload({
    ...vault(),
    proxyProfiles,
  } as SyncableVaultData & { proxyProfiles: typeof proxyProfiles });

  assert.deepEqual(payload.proxyProfiles, proxyProfiles);
});

test("buildSyncPayload includes AI configuration settings", () => {
  const providers = [{
    id: "openai-main",
    providerId: "openai",
    name: "OpenAI",
    apiKey: "enc:v1:test",
    defaultModel: "gpt-test",
    enabled: true,
  }];
  const webSearch = {
    providerId: "tavily",
    apiKey: "enc:v1:web",
    enabled: true,
    maxResults: 7,
  };

  localStorage.setItem(storageKeys.STORAGE_KEY_AI_PROVIDERS, JSON.stringify(providers));
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_ACTIVE_PROVIDER, "openai-main");
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_ACTIVE_MODEL, "gpt-test");
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_PERMISSION_MODE, "autonomous");
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_TOOL_INTEGRATION_MODE, "skills");
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_DEFAULT_AGENT, "codex");
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_COMMAND_BLOCKLIST, JSON.stringify(["rm -rf"]));
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_COMMAND_TIMEOUT, "120");
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_MAX_ITERATIONS, "10");
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_AGENT_MODEL_MAP, JSON.stringify({ codex: "gpt-test" }));
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_AGENT_PROVIDER_MAP, JSON.stringify({ catty: "openai-main" }));
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_WEB_SEARCH, JSON.stringify(webSearch));
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_SHOW_TERMINAL_SELECTION_ACTION, "false");

  const payload = buildSyncPayload(vault([]));

  assert.deepEqual(payload.settings?.ai, {
    providers,
    activeProviderId: "openai-main",
    activeModelId: "gpt-test",
    globalPermissionMode: "autonomous",
    toolIntegrationMode: "skills",
    defaultAgentId: "codex",
    commandBlocklist: ["rm -rf"],
    commandTimeout: 120,
    maxIterations: 10,
    agentModelMap: { codex: "gpt-test" },
    agentProviderMap: { catty: "openai-main" },
    webSearchConfig: webSearch,
    showTerminalSelectionAction: false,
  });
});

test("terminal selection AI preference is syncable for auto-sync detection", () => {
  assert.ok(
    (SYNCABLE_SETTING_STORAGE_KEYS as readonly string[]).includes(
      storageKeys.STORAGE_KEY_AI_SHOW_TERMINAL_SELECTION_ACTION,
    ),
  );
});

test("buildSyncPayload includes host tree sidebar visibility setting", () => {
  localStorage.setItem(storageKeys.STORAGE_KEY_SHOW_HOST_TREE_SIDEBAR, "false");

  const payload = buildSyncPayload(vault([]));

  assert.equal(payload.settings?.showHostTreeSidebar, false);
});

test("buildSyncPayload excludes externalAgents (device-local OS-bound config)", () => {
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_EXTERNAL_AGENTS, JSON.stringify([
    { id: "codex", name: "Codex", command: "/opt/homebrew/bin/codex", enabled: true },
  ]));

  const payload = buildSyncPayload(vault([]));

  assert.equal("ai" in (payload.settings ?? {}), false);
});

test("buildSyncPayload omits device-bound encrypted AI API keys", () => {
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_PROVIDERS, JSON.stringify([{
    id: "openai-main",
    providerId: "openai",
    name: "OpenAI",
    apiKey: "enc:v1:djEwAAAA",
    enabled: true,
  }]));
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_WEB_SEARCH, JSON.stringify({
    providerId: "tavily",
    apiKey: "enc:v1:djEwAAAA",
    enabled: true,
  }));

  const payload = buildSyncPayload(vault([]));

  assert.equal("apiKey" in (payload.settings?.ai?.providers?.[0] ?? {}), false);
  assert.equal("apiKey" in (payload.settings?.ai?.webSearchConfig ?? {}), false);
});

test("applySyncPayload restores AI configuration settings", async () => {
  const providers = [{
    id: "anthropic-main",
    providerId: "anthropic",
    name: "Anthropic",
    apiKey: "enc:v1:test",
    enabled: true,
  }];
  const webSearch = {
    providerId: "exa",
    apiKey: "enc:v1:web",
    enabled: true,
  };

  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    settings: {
      ai: {
        providers,
        activeProviderId: "anthropic-main",
        activeModelId: "claude-test",
        globalPermissionMode: "observer",
        toolIntegrationMode: "mcp",
        defaultAgentId: "claude",
        commandBlocklist: ["shutdown"],
        commandTimeout: 30,
        maxIterations: 5,
        agentModelMap: { claude: "claude-test" },
        agentProviderMap: { catty: "anthropic-main" },
        webSearchConfig: webSearch,
        showTerminalSelectionAction: false,
      },
    },
    syncedAt: 1,
  } as SyncPayload;

  await applySyncPayload(payload, { importVaultData: () => {} });

  assert.deepEqual(JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_PROVIDERS)!), providers);
  assert.equal(localStorage.getItem(storageKeys.STORAGE_KEY_AI_ACTIVE_PROVIDER), "anthropic-main");
  assert.equal(localStorage.getItem(storageKeys.STORAGE_KEY_AI_ACTIVE_MODEL), "claude-test");
  assert.equal(localStorage.getItem(storageKeys.STORAGE_KEY_AI_PERMISSION_MODE), "observer");
  assert.equal(localStorage.getItem(storageKeys.STORAGE_KEY_AI_TOOL_INTEGRATION_MODE), "mcp");
  assert.equal(localStorage.getItem(storageKeys.STORAGE_KEY_AI_DEFAULT_AGENT), "claude");
  assert.deepEqual(JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_COMMAND_BLOCKLIST)!), ["shutdown"]);
  assert.equal(localStorage.getItem(storageKeys.STORAGE_KEY_AI_COMMAND_TIMEOUT), "30");
  assert.equal(localStorage.getItem(storageKeys.STORAGE_KEY_AI_MAX_ITERATIONS), "5");
  assert.deepEqual(JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_AGENT_MODEL_MAP)!), { claude: "claude-test" });
  assert.deepEqual(JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_AGENT_PROVIDER_MAP)!), { catty: "anthropic-main" });
  assert.deepEqual(JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_WEB_SEARCH)!), webSearch);
  assert.equal(localStorage.getItem(storageKeys.STORAGE_KEY_AI_SHOW_TERMINAL_SELECTION_ACTION), "false");
});

test("applySyncPayload restores host tree sidebar visibility setting", async () => {
  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    settings: {
      showHostTreeSidebar: false,
    },
    syncedAt: 1,
  } as SyncPayload;

  await applySyncPayload(payload, { importVaultData: () => {} });

  assert.equal(localStorage.getItem(storageKeys.STORAGE_KEY_SHOW_HOST_TREE_SIDEBAR), "false");
});

test("applySyncPayload dispatches a same-window AI-state-changed event so the open chat panel rehydrates", async () => {
  // Without this nudge, the apply path writes to localStorage but
  // `useAIState` (listening for `storage` events) never sees the changes
  // in the calling window — mounted UI keeps showing pre-sync data.
  const dispatched: Array<{ type: string; detail: unknown }> = [];
  const fakeWindow = {
    addEventListener: () => {},
    removeEventListener: () => {},
    dispatchEvent(event: Event) {
      dispatched.push({
        type: event.type,
        detail: (event as CustomEvent).detail,
      });
      return true;
    },
  };
  Object.defineProperty(globalThis, "window", { value: fakeWindow, configurable: true });
  try {
    localStorage.setItem(storageKeys.STORAGE_KEY_AI_AGENT_PROVIDER_MAP, JSON.stringify({ catty: "deepseek-local" }));
    localStorage.setItem(storageKeys.STORAGE_KEY_AI_AGENT_MODEL_MAP, JSON.stringify({ catty: "deepseek-v4-flash" }));

    const payload: SyncPayload = {
      hosts: [],
      keys: [],
      identities: [],
      snippets: [],
      customGroups: [],
      settings: {
        ai: {
          providers: [{ id: "openai-main", providerId: "openai", name: "OpenAI", enabled: true }],
        },
      },
      syncedAt: 1,
    } as SyncPayload;

    await applySyncPayload(payload, { importVaultData: () => {} });

    const events = dispatched.filter((e) => e.type === "netcatty:ai-state-changed");
    const keys = events.map((e) => (e.detail as { key?: string })?.key);
    assert.ok(keys.includes(storageKeys.STORAGE_KEY_AI_PROVIDERS), "providers nudge");
    assert.ok(keys.includes(storageKeys.STORAGE_KEY_AI_AGENT_PROVIDER_MAP), "agentProviderMap nudge");
    assert.ok(keys.includes(storageKeys.STORAGE_KEY_AI_AGENT_MODEL_MAP), "agentModelMap nudge");
  } finally {
    delete (globalThis as { window?: unknown }).window;
  }
});

test("applySyncPayload prunes per-agent bindings that reference providers absent from the synced set", async () => {
  // Local state has Catty bound to a provider the incoming sync no longer
  // ships — both the per-agent provider override and the saved model should
  // be cleared so we don't dispatch a ghost provider id (or its now-orphan
  // model name) to the wrong endpoint.
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_AGENT_PROVIDER_MAP, JSON.stringify({
    catty: "deepseek-local",
    codex: "openai-main",
  }));
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_AGENT_MODEL_MAP, JSON.stringify({
    catty: "deepseek-v4-flash",
    codex: "gpt-test",
  }));

  const syncedProviders = [
    { id: "openai-main", providerId: "openai", name: "OpenAI", enabled: true },
  ];

  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    settings: {
      ai: {
        providers: syncedProviders,
        // Intentionally omit agentProviderMap — exercises the reconcile path.
      },
    },
    syncedAt: 1,
  } as SyncPayload;

  await applySyncPayload(payload, { importVaultData: () => {} });

  assert.deepEqual(
    JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_AGENT_PROVIDER_MAP)!),
    { codex: "openai-main" },
  );
  // Catty's saved model belonged to the now-missing deepseek-local — drop it.
  // Codex's binding stays, so its saved model stays.
  assert.deepEqual(
    JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_AGENT_MODEL_MAP)!),
    { codex: "gpt-test" },
  );
});

test("applySyncPayload preserves local externalAgents and ignores legacy payload field", async () => {
  const localAgents = [
    { id: "codex", name: "Codex", command: "/usr/local/bin/codex", enabled: true },
  ];
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_EXTERNAL_AGENTS, JSON.stringify(localAgents));

  const payload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    settings: {
      ai: {
        // Legacy snapshot still carries externalAgents; current code must ignore it.
        externalAgents: [
          { id: "claude", name: "Claude", command: "C:\\Tools\\claude.exe", enabled: true },
        ],
      },
    },
    syncedAt: 1,
  } as unknown as SyncPayload;

  await applySyncPayload(payload, { importVaultData: () => {} });

  assert.deepEqual(
    JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_EXTERNAL_AGENTS)!),
    localAgents,
  );
});

test("applySyncPayload preserves local AI provider apiKeys when synced payload omits them", async () => {
  const localProviders = [
    {
      id: "openai-main",
      providerId: "openai",
      name: "OpenAI",
      apiKey: "enc:v1:djEwLOCAL",
      enabled: true,
    },
    {
      id: "anthropic-main",
      providerId: "anthropic",
      name: "Anthropic",
      apiKey: "enc:v1:djEwANTHROPIC",
      enabled: true,
    },
  ];
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_PROVIDERS, JSON.stringify(localProviders));

  // Synced payload mirrors what `collectSyncableSettings` produces on another device:
  // metadata is preserved but encrypted device-bound apiKeys are stripped.
  const syncedProviders = [
    { id: "openai-main", providerId: "openai", name: "OpenAI (renamed)", enabled: true },
    { id: "anthropic-main", providerId: "anthropic", name: "Anthropic", enabled: false },
  ];

  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    settings: { ai: { providers: syncedProviders } },
    syncedAt: 1,
  } as SyncPayload;

  await applySyncPayload(payload, { importVaultData: () => {} });

  const stored = JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_PROVIDERS)!);
  assert.deepEqual(stored, [
    {
      id: "openai-main",
      providerId: "openai",
      name: "OpenAI (renamed)",
      apiKey: "enc:v1:djEwLOCAL",
      enabled: true,
    },
    {
      id: "anthropic-main",
      providerId: "anthropic",
      name: "Anthropic",
      apiKey: "enc:v1:djEwANTHROPIC",
      enabled: false,
    },
  ]);
});

test("applySyncPayload prefers explicit synced apiKey over local apiKey", async () => {
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_PROVIDERS, JSON.stringify([
    { id: "openai-main", providerId: "openai", name: "OpenAI", apiKey: "enc:v1:djEwLOCAL", enabled: true },
  ]));

  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    settings: {
      ai: {
        providers: [
          { id: "openai-main", providerId: "openai", name: "OpenAI", apiKey: "plaintext-from-other-device", enabled: true },
        ],
      },
    },
    syncedAt: 1,
  } as SyncPayload;

  await applySyncPayload(payload, { importVaultData: () => {} });

  const stored = JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_PROVIDERS)!);
  assert.equal(stored[0].apiKey, "plaintext-from-other-device");
});

test("applySyncPayload preserves local web-search apiKey when synced config omits it", async () => {
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_WEB_SEARCH, JSON.stringify({
    providerId: "tavily",
    apiKey: "enc:v1:djEwWEB",
    enabled: true,
    maxResults: 7,
  }));

  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    settings: {
      ai: {
        webSearchConfig: { providerId: "tavily", enabled: false, maxResults: 12 },
      },
    },
    syncedAt: 1,
  } as SyncPayload;

  await applySyncPayload(payload, { importVaultData: () => {} });

  const stored = JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_WEB_SEARCH)!);
  assert.deepEqual(stored, {
    providerId: "tavily",
    apiKey: "enc:v1:djEwWEB",
    enabled: false,
    maxResults: 12,
  });
});

test("applySyncPayload drops local web-search apiKey when synced config switches provider", async () => {
  localStorage.setItem(storageKeys.STORAGE_KEY_AI_WEB_SEARCH, JSON.stringify({
    providerId: "tavily",
    apiKey: "enc:v1:djEwWEB",
    enabled: true,
  }));

  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    settings: {
      ai: {
        webSearchConfig: { providerId: "exa", enabled: true },
      },
    },
    syncedAt: 1,
  } as SyncPayload;

  await applySyncPayload(payload, { importVaultData: () => {} });

  const stored = JSON.parse(localStorage.getItem(storageKeys.STORAGE_KEY_AI_WEB_SEARCH)!);
  assert.equal("apiKey" in stored, false);
  assert.equal(stored.providerId, "exa");
});

test("buildSyncPayload includes syncable terminal options from settings", () => {
  localStorage.setItem(storageKeys.STORAGE_KEY_TERM_FOLLOW_APP_THEME, "true");
  localStorage.setItem(storageKeys.STORAGE_KEY_TERM_SETTINGS, JSON.stringify({
    terminalEmulationType: "vt100",
    altAsMeta: true,
    showServerStats: false,
    serverStatsRefreshInterval: 12,
    rendererType: "dom",
    localShell: "/bin/zsh",
  }));

  const payload = buildSyncPayload(vault([]));

  assert.equal(payload.settings?.followAppTerminalTheme, true);
  assert.deepEqual(payload.settings?.terminalSettings, {
    terminalEmulationType: "vt100",
    altAsMeta: true,
    showServerStats: false,
    serverStatsRefreshInterval: 12,
    rendererType: "dom",
  });
});

test("hasMeaningfulCloudSyncData ignores legacy cloud known hosts", () => {
  assert.equal(
    hasMeaningfulCloudSyncData({
      hosts: [],
      keys: [],
      identities: [],
      snippets: [],
      customGroups: [],
      knownHosts: [knownHost("kh-only")],
      syncedAt: 1,
    }),
    false,
  );
});

test("hasCloudSyncEntityData ignores settings-only payloads for empty-vault recovery", () => {
  assert.equal(
    hasCloudSyncEntityData({
      hosts: [],
      keys: [],
      identities: [],
      snippets: [],
      customGroups: [],
      settings: { theme: "system", terminalTheme: "default" },
      syncedAt: 1,
    }),
    false,
  );
});

test("shouldPromptCloudVaultRecovery ignores settings-only remote payloads", () => {
  const settingsOnlyPayload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    settings: { theme: "system", terminalTheme: "default" },
    syncedAt: 1,
  };

  assert.equal(
    shouldPromptCloudVaultRecovery(settingsOnlyPayload, settingsOnlyPayload),
    false,
  );
});

test("buildLocalVaultPayload preserves known hosts for local backups", () => {
  const payload = buildLocalVaultPayload(vault([knownHost("kh-local")]));

  assert.deepEqual(payload.knownHosts, [knownHost("kh-local")]);
});

test("applySyncPayload ignores legacy cloud known hosts", async () => {
  let imported: Record<string, unknown> | null = null;
  const proxyProfiles = [
    {
      id: "proxy-1",
      label: "Office Proxy",
      config: { type: "socks5", host: "proxy.example.com", port: 1080 },
      createdAt: 1,
      updatedAt: 1,
    },
  ];
  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    knownHosts: [knownHost("kh-legacy")],
    proxyProfiles,
    syncedAt: 1,
  } as SyncPayload & { proxyProfiles: typeof proxyProfiles };

  await applySyncPayload(payload, {
    importVaultData: (json) => {
      imported = JSON.parse(json);
    },
  });

  assert.ok(imported);
  assert.equal("knownHosts" in imported, false);
  assert.deepEqual(imported.proxyProfiles, proxyProfiles);
});

test("applySyncPayload keeps missing proxy references visible to connection guards", async () => {
  let imported: Record<string, unknown> | null = null;
  const payload: SyncPayload = {
    hosts: [{
      id: "host-1",
      label: "Host",
      hostname: "example.com",
      username: "root",
      tags: [],
      os: "linux",
      proxyProfileId: "missing-proxy",
    }],
    keys: [],
    identities: [],
    proxyProfiles: [],
    snippets: [],
    customGroups: [],
    groupConfigs: [{ path: "prod", proxyProfileId: "missing-proxy" }],
    syncedAt: 1,
  };

  await applySyncPayload(payload, {
    importVaultData: (json) => {
      imported = JSON.parse(json);
    },
  });

  assert.ok(imported);
  assert.equal((imported.hosts as SyncPayload["hosts"])[0]?.proxyProfileId, "missing-proxy");
  assert.equal((imported.groupConfigs as SyncPayload["groupConfigs"])?.[0]?.proxyProfileId, "missing-proxy");
});

test("applySyncPayload preserves host proxy references when group configs are absent", async () => {
  let imported: Record<string, unknown> | null = null;
  const payload: SyncPayload = {
    hosts: [{
      id: "host-1",
      label: "Host",
      hostname: "example.com",
      username: "root",
      tags: [],
      os: "linux",
      proxyProfileId: "missing-proxy",
    }],
    keys: [],
    identities: [],
    proxyProfiles: [],
    snippets: [],
    customGroups: [],
    syncedAt: 1,
  };

  await applySyncPayload(payload, {
    importVaultData: (json) => {
      imported = JSON.parse(json);
    },
  });

  assert.ok(imported);
  assert.equal((imported.hosts as SyncPayload["hosts"])[0]?.proxyProfileId, "missing-proxy");
  assert.equal("groupConfigs" in imported, false);
});

test("applySyncPayload migrates legacy global line timestamps onto hosts", async () => {
  let imported: Record<string, unknown> | null = null;
  const payload: SyncPayload = {
    hosts: [
      {
        id: "host-1",
        label: "Inherited",
        hostname: "example.com",
        username: "root",
        tags: [],
        os: "linux",
      },
      {
        id: "host-2",
        label: "Explicit",
        hostname: "example.net",
        username: "root",
        tags: [],
        os: "linux",
        showLineTimestamps: false,
      },
    ],
    keys: [],
    identities: [],
    proxyProfiles: [],
    snippets: [],
    customGroups: [],
    syncedAt: 1,
    settings: { terminalSettings: { showLineTimestamps: true } },
  };

  await applySyncPayload(payload, {
    importVaultData: (json) => {
      imported = JSON.parse(json);
    },
  });

  assert.ok(imported);
  const hosts = imported.hosts as SyncPayload["hosts"];
  assert.equal(hosts[0]?.showLineTimestamps, true);
  assert.equal(hosts[1]?.showLineTimestamps, false);
});

test("applySyncPayload waits for async vault imports", async () => {
  let finished = false;
  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    syncedAt: 1,
  };

  const promise = applySyncPayload(payload, {
    importVaultData: async () => {
      await new Promise((resolve) => setTimeout(resolve, 1));
      finished = true;
    },
  });

  assert.equal(finished, false);
  await promise;
  assert.equal(finished, true);
});

test("buildSyncPayload includes fallbackFont when present in TERM_SETTINGS", () => {
  localStorage.setItem(
    storageKeys.STORAGE_KEY_TERM_SETTINGS,
    JSON.stringify({ scrollback: 5000, fallbackFont: "PingFang SC", fontLigatures: true }),
  );

  const payload = buildSyncPayload(vault());
  const termSettings = (payload.settings?.terminalSettings ?? {}) as Record<string, unknown>;
  assert.equal(termSettings.fallbackFont, "PingFang SC");
});

test("buildSyncPayload omits fallbackFont when TERM_SETTINGS does not set it", () => {
  localStorage.setItem(
    storageKeys.STORAGE_KEY_TERM_SETTINGS,
    JSON.stringify({ scrollback: 5000, fontLigatures: true }),
  );

  const payload = buildSyncPayload(vault());
  const termSettings = (payload.settings?.terminalSettings ?? {}) as Record<string, unknown>;
  assert.equal("fallbackFont" in termSettings, false);
});

test("applySyncPayload writes incoming fallbackFont into local TERM_SETTINGS", async () => {
  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    syncedAt: 1,
    settings: { terminalSettings: { fallbackFont: "Sarasa Mono SC" } },
  };

  await applySyncPayload(payload, {
    importVaultData: () => {},
  });

  const raw = localStorage.getItem(storageKeys.STORAGE_KEY_TERM_SETTINGS);
  assert.ok(raw, "TERM_SETTINGS should be written");
  const parsed = JSON.parse(raw!);
  assert.equal(parsed.fallbackFont, "Sarasa Mono SC");
});

test("applySyncPayload from legacy client (no fallbackFont) preserves local value", async () => {
  localStorage.setItem(
    storageKeys.STORAGE_KEY_TERM_SETTINGS,
    JSON.stringify({ scrollback: 5000, fallbackFont: "Microsoft YaHei UI" }),
  );

  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    syncedAt: 1,
    settings: { terminalSettings: { scrollback: 9999 } },
  };

  await applySyncPayload(payload, {
    importVaultData: () => {},
  });

  const raw = localStorage.getItem(storageKeys.STORAGE_KEY_TERM_SETTINGS);
  const parsed = JSON.parse(raw!);
  assert.equal(parsed.fallbackFont, "Microsoft YaHei UI", "legacy payload must not wipe local fallbackFont");
  assert.equal(parsed.scrollback, 9999);
});

test("applyLocalVaultPayload restores known hosts from local backups", async () => {
  let imported: Record<string, unknown> | null = null;
  const payload: SyncPayload = {
    hosts: [],
    keys: [],
    identities: [],
    snippets: [],
    customGroups: [],
    knownHosts: [knownHost("kh-backup")],
    syncedAt: 1,
  };

  await applyLocalVaultPayload(payload, {
    importVaultData: (json) => {
      imported = JSON.parse(json);
    },
  });

  assert.ok(imported);
  assert.deepEqual(imported.knownHosts, [knownHost("kh-backup")]);
});
