import { describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
  loadSessionStore: vi.fn().mockReturnValue({
    "+1000": {
      updatedAt: Date.now() - 60_000,
      verboseLevel: "on",
      thinkingLevel: "low",
      inputTokens: 2_000,
      outputTokens: 3_000,
      totalTokens: 5_000,
      contextTokens: 10_000,
      model: "pi:opus",
      sessionId: "abc123",
      systemSent: true,
    },
  }),
  resolveMainSessionKey: vi.fn().mockReturnValue("agent:main:main"),
  resolveStorePath: vi.fn().mockReturnValue("/tmp/sessions.json"),
  webAuthExists: vi.fn().mockResolvedValue(true),
  getWebAuthAgeMs: vi.fn().mockReturnValue(5000),
  readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
  logWebSelfId: vi.fn(),
  probeGateway: vi.fn().mockResolvedValue({
    ok: false,
    url: "ws://127.0.0.1:18789",
    connectLatencyMs: null,
    error: "timeout",
    close: null,
    health: null,
    status: null,
    presence: null,
    configSnapshot: null,
  }),
  callGateway: vi.fn().mockResolvedValue({}),
  listAgentsForGateway: vi.fn().mockReturnValue({
    defaultId: "main",
    mainKey: "agent:main:main",
    scope: "per-sender",
    agents: [{ id: "main", name: "Main" }],
  }),
  runSecurityAudit: vi.fn().mockResolvedValue({
    ts: 0,
    summary: { critical: 1, warn: 1, info: 2 },
    findings: [
      {
        checkId: "test.critical",
        severity: "critical",
        title: "Test critical finding",
        detail: "Something is very wrong\nbut on two lines",
        remediation: "Do the thing",
      },
      {
        checkId: "test.warn",
        severity: "warn",
        title: "Test warning finding",
        detail: "Something is maybe wrong",
      },
      {
        checkId: "test.info",
        severity: "info",
        title: "Test info finding",
        detail: "FYI only",
      },
      {
        checkId: "test.info2",
        severity: "info",
        title: "Another info finding",
        detail: "More FYI",
      },
    ],
  }),
}));

vi.mock("../memory/manager.js", () => ({
  MemoryIndexManager: {
    get: vi.fn(async ({ agentId }: { agentId: string }) => ({
      probeVectorAvailability: vi.fn(async () => true),
      status: () => ({
        files: 2,
        chunks: 3,
        dirty: false,
        workspaceDir: "/tmp/clawd",
        dbPath: "/tmp/memory.sqlite",
        provider: "openai",
        model: "text-embedding-3-small",
        requestedProvider: "openai",
        sources: ["memory"],
        sourceCounts: [{ source: "memory", files: 2, chunks: 3 }],
        cache: { enabled: true, entries: 10, maxEntries: 500 },
        fts: { enabled: true, available: true },
        vector: { enabled: true, available: true, extensionPath: "/opt/vec0.dylib", dims: 1024 },
      }),
      close: vi.fn(async () => {}),
      __agentId: agentId,
    })),
  },
}));

vi.mock("../config/sessions.js", () => ({
  loadSessionStore: mocks.loadSessionStore,
  resolveMainSessionKey: mocks.resolveMainSessionKey,
  resolveStorePath: mocks.resolveStorePath,
  resolveFreshSessionTotalTokens: vi.fn(
    (entry?: { totalTokens?: number; totalTokensFresh?: boolean }) =>
      typeof entry?.totalTokens === "number" && entry?.totalTokensFresh !== false
        ? entry.totalTokens
        : undefined,
  ),
  readSessionUpdatedAt: vi.fn(() => undefined),
  recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../channels/plugins/index.js", () => ({
  listChannelPlugins: () =>
    [
      {
        id: "whatsapp",
        meta: {
          id: "whatsapp",
          label: "WhatsApp",
          selectionLabel: "WhatsApp",
          docsPath: "/platforms/whatsapp",
          blurb: "mock",
        },
        config: {
          listAccountIds: () => ["default"],
          resolveAccount: () => ({}),
        },
        status: {
          buildChannelSummary: async () => ({ linked: true, authAgeMs: 5000 }),
        },
      },
      {
        id: "signal",
        meta: {
          id: "signal",
          label: "Signal",
          selectionLabel: "Signal",
          docsPath: "/platforms/signal",
          blurb: "mock",
        },
        config: {
          listAccountIds: () => ["default"],
          resolveAccount: () => ({}),
        },
        status: {
          collectStatusIssues: (accounts: Array<Record<string, unknown>>) =>
            accounts
              .filter((account) => typeof account.lastError === "string" && account.lastError)
              .map((account) => ({
                channel: "signal",
                accountId: typeof account.accountId === "string" ? account.accountId : "default",
                message: `Channel error: ${String(account.lastError)}`,
              })),
        },
      },
      {
        id: "imessage",
        meta: {
          id: "imessage",
          label: "iMessage",
          selectionLabel: "iMessage",
          docsPath: "/platforms/mac",
          blurb: "mock",
        },
        config: {
          listAccountIds: () => ["default"],
          resolveAccount: () => ({}),
        },
        status: {
          collectStatusIssues: (accounts: Array<Record<string, unknown>>) =>
            accounts
              .filter((account) => typeof account.lastError === "string" && account.lastError)
              .map((account) => ({
                channel: "imessage",
                accountId: typeof account.accountId === "string" ? account.accountId : "default",
                message: `Channel error: ${String(account.lastError)}`,
              })),
        },
      },
    ] as unknown,
}));
vi.mock("../web/session.js", () => ({
  webAuthExists: mocks.webAuthExists,
  getWebAuthAgeMs: mocks.getWebAuthAgeMs,
  readWebSelfId: mocks.readWebSelfId,
  logWebSelfId: mocks.logWebSelfId,
}));
vi.mock("../gateway/probe.js", () => ({
  probeGateway: mocks.probeGateway,
}));
vi.mock("../gateway/call.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../gateway/call.js")>();
  return { ...actual, callGateway: mocks.callGateway };
});
vi.mock("../gateway/session-utils.js", () => ({
  listAgentsForGateway: mocks.listAgentsForGateway,
}));
vi.mock("../infra/clawdbot-root.js", () => ({
  resolveClawdbotPackageRoot: vi.fn().mockResolvedValue("/tmp/clawdbot"),
}));
vi.mock("../infra/os-summary.js", () => ({
  resolveOsSummary: () => ({
    platform: "darwin",
    arch: "arm64",
    release: "23.0.0",
    label: "macos 14.0 (arm64)",
  }),
}));
vi.mock("../infra/update-check.js", () => ({
  checkUpdateStatus: vi.fn().mockResolvedValue({
    root: "/tmp/clawdbot",
    installKind: "git",
    packageManager: "pnpm",
    git: {
      root: "/tmp/clawdbot",
      branch: "main",
      upstream: "origin/main",
      dirty: false,
      ahead: 0,
      behind: 0,
      fetchOk: true,
    },
    deps: {
      manager: "pnpm",
      status: "ok",
      lockfilePath: "/tmp/clawdbot/pnpm-lock.yaml",
      markerPath: "/tmp/clawdbot/node_modules/.modules.yaml",
    },
    registry: { latestVersion: "0.0.0" },
  }),
  compareSemverStrings: vi.fn(() => 0),
}));
vi.mock("../config/config.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../config/config.js")>();
  return {
    ...actual,
    loadConfig: () => ({ session: {} }),
  };
});
vi.mock("../daemon/service.js", () => ({
  resolveGatewayService: () => ({
    label: "LaunchAgent",
    loadedText: "loaded",
    notLoadedText: "not loaded",
    isLoaded: async () => true,
    readRuntime: async () => ({ status: "running", pid: 1234 }),
    readCommand: async () => ({
      programArguments: ["node", "dist/entry.js", "gateway"],
      sourcePath: "/tmp/Library/LaunchAgents/com.clawdbot.gateway.plist",
    }),
  }),
}));
vi.mock("../daemon/node-service.js", () => ({
  resolveNodeService: () => ({
    label: "LaunchAgent",
    loadedText: "loaded",
    notLoadedText: "not loaded",
    isLoaded: async () => true,
    readRuntime: async () => ({ status: "running", pid: 4321 }),
    readCommand: async () => ({
      programArguments: ["node", "dist/entry.js", "node-host"],
      sourcePath: "/tmp/Library/LaunchAgents/com.clawdbot.node.plist",
    }),
  }),
}));
vi.mock("../security/audit.js", () => ({
  runSecurityAudit: mocks.runSecurityAudit,
}));

import { statusCommand } from "./status.js";

const runtime = {
  log: vi.fn(),
  error: vi.fn(),
  exit: vi.fn(),
};

describe("statusCommand", () => {
  it("prints JSON when requested", async () => {
    await statusCommand({ json: true }, runtime as never);
    const payload = JSON.parse((runtime.log as vi.Mock).mock.calls[0][0]);
    expect(payload.linkChannel.linked).toBe(true);
    expect(payload.memory.agentId).toBe("main");
    expect(payload.memoryPlugin.enabled).toBe(true);
    expect(payload.memoryPlugin.slot).toBe("memory-core");
    expect(payload.memory.vector.available).toBe(true);
    expect(payload.sessions.count).toBe(1);
    expect(payload.sessions.paths).toContain("/tmp/sessions.json");
    expect(payload.sessions.defaults.model).toBeTruthy();
    expect(payload.sessions.defaults.contextTokens).toBeGreaterThan(0);
    expect(payload.sessions.recent[0].percentUsed).toBe(50);
    expect(payload.sessions.recent[0].totalTokensFresh).toBe(true);
    expect(payload.sessions.recent[0].remainingTokens).toBe(5000);
    expect(payload.sessions.recent[0].flags).toContain("verbose:on");
    expect(payload.securityAudit.summary.critical).toBe(1);
    expect(payload.securityAudit.summary.warn).toBe(1);
    expect(payload.gatewayService.label).toBe("LaunchAgent");
    expect(payload.nodeService.label).toBe("LaunchAgent");
  });

  it("surfaces unknown usage when totalTokens is missing", async () => {
    const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation();
    mocks.loadSessionStore.mockReturnValue({
      "+1000": {
        updatedAt: Date.now() - 60_000,
        inputTokens: 2_000,
        outputTokens: 3_000,
        contextTokens: 10_000,
        model: "pi:opus",
      },
    });

    (runtime.log as vi.Mock).mockClear();
    await statusCommand({ json: true }, runtime as never);
    const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]);
    expect(payload.sessions.recent[0].totalTokens).toBeNull();
    expect(payload.sessions.recent[0].totalTokensFresh).toBe(false);
    expect(payload.sessions.recent[0].percentUsed).toBeNull();
    expect(payload.sessions.recent[0].remainingTokens).toBeNull();

    if (originalLoadSessionStore) {
      mocks.loadSessionStore.mockImplementation(originalLoadSessionStore);
    }
  });

  it("prints unknown usage in formatted output when totalTokens is missing", async () => {
    const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation();
    mocks.loadSessionStore.mockReturnValue({
      "+1000": {
        updatedAt: Date.now() - 60_000,
        inputTokens: 2_000,
        outputTokens: 3_000,
        contextTokens: 10_000,
        model: "pi:opus",
      },
    });

    try {
      (runtime.log as vi.Mock).mockClear();
      await statusCommand({}, runtime as never);
      const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0]));
      expect(logs.some((line) => line.includes("unknown/") && line.includes("(?%)"))).toBe(true);
    } finally {
      if (originalLoadSessionStore) {
        mocks.loadSessionStore.mockImplementation(originalLoadSessionStore);
      }
    }
  });

  it("prints formatted lines otherwise", async () => {
    (runtime.log as vi.Mock).mockClear();
    await statusCommand({}, runtime as never);
    const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0]));
    expect(logs.some((l) => l.includes("Clawdbot status"))).toBe(true);
    expect(logs.some((l) => l.includes("Overview"))).toBe(true);
    expect(logs.some((l) => l.includes("Security audit"))).toBe(true);
    expect(logs.some((l) => l.includes("Summary:"))).toBe(true);
    expect(logs.some((l) => l.includes("CRITICAL"))).toBe(true);
    expect(logs.some((l) => l.includes("Dashboard"))).toBe(true);
    expect(logs.some((l) => l.includes("macos 14.0 (arm64)"))).toBe(true);
    expect(logs.some((l) => l.includes("Memory"))).toBe(true);
    expect(logs.some((l) => l.includes("Channels"))).toBe(true);
    expect(logs.some((l) => l.includes("WhatsApp"))).toBe(true);
    expect(logs.some((l) => l.includes("Sessions"))).toBe(true);
    expect(logs.some((l) => l.includes("+1000"))).toBe(true);
    expect(logs.some((l) => l.includes("50%"))).toBe(true);
    expect(logs.some((l) => l.includes("LaunchAgent"))).toBe(true);
    expect(logs.some((l) => l.includes("FAQ:"))).toBe(true);
    expect(logs.some((l) => l.includes("Troubleshooting:"))).toBe(true);
    expect(logs.some((l) => l.includes("Next steps:"))).toBe(true);
    expect(logs.some((l) => l.includes("openclaw-cn status --all"))).toBe(true);
  });

  it("shows gateway auth when reachable", async () => {
    const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
    process.env.OPENCLAW_GATEWAY_TOKEN = "abcd1234";
    try {
      mocks.probeGateway.mockResolvedValueOnce({
        ok: true,
        url: "ws://127.0.0.1:18789",
        connectLatencyMs: 123,
        error: null,
        close: null,
        health: {},
        status: {},
        presence: [],
        configSnapshot: null,
      });
      (runtime.log as vi.Mock).mockClear();
      await statusCommand({}, runtime as never);
      const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0]));
      expect(logs.some((l) => l.includes("auth token"))).toBe(true);
    } finally {
      if (prevToken === undefined) delete process.env.OPENCLAW_GATEWAY_TOKEN;
      else process.env.OPENCLAW_GATEWAY_TOKEN = prevToken;
    }
  });

  it("surfaces channel runtime errors from the gateway", async () => {
    mocks.probeGateway.mockResolvedValueOnce({
      ok: true,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: 10,
      error: null,
      close: null,
      health: {},
      status: {},
      presence: [],
      configSnapshot: null,
    });
    mocks.callGateway.mockResolvedValueOnce({
      channelAccounts: {
        signal: [
          {
            accountId: "default",
            enabled: true,
            configured: true,
            running: false,
            lastError: "signal-cli unreachable",
          },
        ],
        imessage: [
          {
            accountId: "default",
            enabled: true,
            configured: true,
            running: false,
            lastError: "imessage permission denied",
          },
        ],
      },
    });

    (runtime.log as vi.Mock).mockClear();
    await statusCommand({}, runtime as never);
    const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0]));
    expect(logs.join("\n")).toMatch(/Signal/i);
    expect(logs.join("\n")).toMatch(/iMessage/i);
    expect(logs.join("\n")).toMatch(/gateway:/i);
    expect(logs.join("\n")).toMatch(/WARN/);
  });

  it("prints requestId-aware recovery guidance when gateway pairing is required", async () => {
    mocks.probeGateway.mockResolvedValueOnce({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "connect failed: pairing required (requestId: req-123)",
      close: { code: 1008, reason: "pairing required (requestId: req-123)" },
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    (runtime.log as vi.Mock).mockClear();
    await statusCommand({}, runtime as never);
    const logs = (runtime.log as vi.Mock).mock.calls.map((c: unknown[]) => String(c[0]));
    const joined = logs.join("\n");
    expect(joined).toContain("需要网关配对审批。");
    expect(joined).toContain("devices approve req-123");
    expect(joined).toContain("devices approve --latest");
    expect(joined).toContain("devices list");
  });

  it("prints fallback recovery guidance when pairing requestId is unavailable", async () => {
    mocks.probeGateway.mockResolvedValueOnce({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "connect failed: pairing required",
      close: { code: 1008, reason: "connect failed" },
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    (runtime.log as vi.Mock).mockClear();
    await statusCommand({}, runtime as never);
    const logs = (runtime.log as vi.Mock).mock.calls.map((c: unknown[]) => String(c[0]));
    const joined = logs.join("\n");
    expect(joined).toContain("需要网关配对审批。");
    expect(joined).not.toContain("devices approve req-");
    expect(joined).toContain("devices approve --latest");
    expect(joined).toContain("devices list");
  });

  it("does not render unsafe requestId content into approval command hints", async () => {
    mocks.probeGateway.mockResolvedValueOnce({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "connect failed: pairing required (requestId: req-123;rm -rf /)",
      close: { code: 1008, reason: "pairing required (requestId: req-123;rm -rf /)" },
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    (runtime.log as vi.Mock).mockClear();
    await statusCommand({}, runtime as never);
    const joined = (runtime.log as vi.Mock).mock.calls.map((c: unknown[]) => String(c[0])).join("\n");
    expect(joined).toContain("需要网关配对审批。");
    expect(joined).not.toContain("devices approve req-123;rm -rf /");
    expect(joined).toContain("devices approve --latest");
  });

  it("extracts requestId from close reason when error text omits it", async () => {
    mocks.probeGateway.mockResolvedValueOnce({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "connect failed: pairing required",
      close: { code: 1008, reason: "pairing required (requestId: req-close-456)" },
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    (runtime.log as vi.Mock).mockClear();
    await statusCommand({}, runtime as never);
    const joined = (runtime.log as vi.Mock).mock.calls.map((c: unknown[]) => String(c[0])).join("\n");
    expect(joined).toContain("devices approve req-close-456");
  });

  it("includes sessions across agents in JSON output", async () => {
    const originalAgents = mocks.listAgentsForGateway.getMockImplementation();
    const originalResolveStorePath = mocks.resolveStorePath.getMockImplementation();
    const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation();

    mocks.listAgentsForGateway.mockReturnValue({
      defaultId: "main",
      mainKey: "agent:main:main",
      scope: "per-sender",
      agents: [
        { id: "main", name: "Main" },
        { id: "ops", name: "Ops" },
      ],
    });
    mocks.resolveStorePath.mockImplementation((_store, opts) =>
      opts?.agentId === "ops" ? "/tmp/ops.json" : "/tmp/main.json",
    );
    mocks.loadSessionStore.mockImplementation((storePath) => {
      if (storePath === "/tmp/ops.json") {
        return {
          "agent:ops:main": {
            updatedAt: Date.now() - 120_000,
            inputTokens: 1_000,
            outputTokens: 1_000,
            totalTokens: 2_000,
            contextTokens: 10_000,
            model: "pi:opus",
          },
        };
      }
      return {
        "+1000": {
          updatedAt: Date.now() - 60_000,
          verboseLevel: "on",
          thinkingLevel: "low",
          inputTokens: 2_000,
          outputTokens: 3_000,
          totalTokens: 5_000,
          contextTokens: 10_000,
          model: "pi:opus",
          sessionId: "abc123",
          systemSent: true,
        },
      };
    });

    await statusCommand({ json: true }, runtime as never);
    const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]);
    expect(payload.sessions.count).toBe(2);
    expect(payload.sessions.paths.length).toBe(2);
    expect(
      payload.sessions.recent.some((sess: { key?: string }) => sess.key === "agent:ops:main"),
    ).toBe(true);

    if (originalAgents) mocks.listAgentsForGateway.mockImplementation(originalAgents);
    if (originalResolveStorePath)
      mocks.resolveStorePath.mockImplementation(originalResolveStorePath);
    if (originalLoadSessionStore)
      mocks.loadSessionStore.mockImplementation(originalLoadSessionStore);
  });
});
