import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { loggingState } from "../logging/state.js";

const mocks = vi.hoisted(() => ({
  resolveConfigPath: vi.fn(() => `/tmp/openclaw-status-scan-missing-${process.pid}.json`),
  hasPotentialConfiguredChannels: vi.fn(),
  readBestEffortConfig: vi.fn(),
  resolveCommandSecretRefsViaGateway: vi.fn(),
  buildChannelsTable: vi.fn(),
  callGateway: vi.fn(),
  getUpdateCheckResult: vi.fn(),
  getAgentLocalStatuses: vi.fn(),
  getStatusSummary: vi.fn(),
  getMemorySearchManager: vi.fn(),
  buildGatewayConnectionDetails: vi.fn(),
  probeGateway: vi.fn(),
  resolveGatewayProbeAuthResolution: vi.fn(),
  ensurePluginRegistryLoaded: vi.fn(),
  buildPluginCompatibilityNotices: vi.fn(() => []),
}));

let originalForceStderr: boolean;

beforeEach(() => {
  vi.clearAllMocks();
  originalForceStderr = loggingState.forceConsoleToStderr;
  loggingState.forceConsoleToStderr = false;
  mocks.hasPotentialConfiguredChannels.mockReturnValue(false);
});

afterEach(() => {
  loggingState.forceConsoleToStderr = originalForceStderr;
});

vi.mock("../channels/config-presence.js", () => ({
  hasPotentialConfiguredChannels: mocks.hasPotentialConfiguredChannels,
}));

vi.mock("../cli/progress.js", () => ({
  withProgress: vi.fn(async (_opts, run) => await run({ setLabel: vi.fn(), tick: vi.fn() })),
}));

vi.mock("../config/config.js", () => ({
  readBestEffortConfig: mocks.readBestEffortConfig,
}));

vi.mock("../config/paths.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../config/paths.js")>();
  return {
    ...actual,
    resolveConfigPath: mocks.resolveConfigPath,
  };
});

vi.mock("../cli/command-secret-gateway.js", () => ({
  resolveCommandSecretRefsViaGateway: mocks.resolveCommandSecretRefsViaGateway,
}));

vi.mock("./status-all/channels.js", () => ({
  buildChannelsTable: mocks.buildChannelsTable,
}));

vi.mock("./status.scan.runtime.js", () => ({
  statusScanRuntime: {
    buildChannelsTable: mocks.buildChannelsTable,
    collectChannelStatusIssues: vi.fn(() => []),
  },
}));

vi.mock("./status.update.js", () => ({
  getUpdateCheckResult: mocks.getUpdateCheckResult,
}));

vi.mock("./status.agent-local.js", () => ({
  getAgentLocalStatuses: mocks.getAgentLocalStatuses,
}));

vi.mock("./status.summary.js", () => ({
  getStatusSummary: mocks.getStatusSummary,
}));

vi.mock("../infra/os-summary.js", () => ({
  resolveOsSummary: vi.fn(() => ({ label: "test-os" })),
}));

vi.mock("./status.scan.deps.runtime.js", () => ({
  getTailnetHostname: vi.fn(),
  getMemorySearchManager: mocks.getMemorySearchManager,
}));

vi.mock("../gateway/call.js", () => ({
  buildGatewayConnectionDetails: mocks.buildGatewayConnectionDetails,
  callGateway: mocks.callGateway,
}));

vi.mock("../gateway/probe.js", () => ({
  probeGateway: mocks.probeGateway,
}));

vi.mock("./status.gateway-probe.js", () => ({
  pickGatewaySelfPresence: vi.fn(() => null),
  resolveGatewayProbeAuthResolution: mocks.resolveGatewayProbeAuthResolution,
}));

vi.mock("../process/exec.js", () => ({
  runExec: vi.fn(),
}));

vi.mock("../cli/plugin-registry.js", () => ({
  ensurePluginRegistryLoaded: mocks.ensurePluginRegistryLoaded,
}));

vi.mock("../plugins/status.js", () => ({
  buildPluginCompatibilityNotices: mocks.buildPluginCompatibilityNotices,
}));

import { scanStatus } from "./status.scan.js";

describe("scanStatus", () => {
  it("passes sourceConfig into buildChannelsTable for summary-mode status output", async () => {
    mocks.readBestEffortConfig.mockResolvedValue({
      marker: "source",
      session: {},
      plugins: { enabled: false },
      gateway: {},
    });
    mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
      resolvedConfig: {
        marker: "resolved",
        session: {},
        plugins: { enabled: false },
        gateway: {},
      },
      diagnostics: [],
    });
    mocks.getUpdateCheckResult.mockResolvedValue({
      installKind: "git",
      git: null,
      registry: null,
    });
    mocks.getAgentLocalStatuses.mockResolvedValue({
      defaultId: "main",
      agents: [],
    });
    mocks.getStatusSummary.mockResolvedValue({
      linkChannel: { linked: false },
      sessions: { count: 0, paths: [], defaults: {}, recent: [] },
    });
    mocks.buildGatewayConnectionDetails.mockReturnValue({
      url: "ws://127.0.0.1:18789",
      urlSource: "default",
    });
    mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
      auth: {},
      warning: undefined,
    });
    mocks.probeGateway.mockResolvedValue({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "timeout",
      close: null,
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });
    mocks.buildChannelsTable.mockResolvedValue({
      rows: [],
      details: [],
    });

    await scanStatus({ json: false }, {} as never);

    expect(mocks.buildChannelsTable).toHaveBeenCalledWith(
      expect.objectContaining({ marker: "resolved" }),
      expect.objectContaining({
        sourceConfig: expect.objectContaining({ marker: "source" }),
      }),
    );
  });

  it("skips channel plugin preload for status --json with no channel config", async () => {
    mocks.readBestEffortConfig.mockResolvedValue({
      session: {},
      plugins: { enabled: false },
      gateway: {},
    });
    mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
      resolvedConfig: {
        session: {},
        plugins: { enabled: false },
        gateway: {},
      },
      diagnostics: [],
    });
    mocks.getUpdateCheckResult.mockResolvedValue({
      installKind: "git",
      git: null,
      registry: null,
    });
    mocks.getAgentLocalStatuses.mockResolvedValue({
      defaultId: "main",
      agents: [],
    });
    mocks.getStatusSummary.mockResolvedValue({
      linkChannel: undefined,
      sessions: { count: 0, paths: [], defaults: {}, recent: [] },
    });
    mocks.buildGatewayConnectionDetails.mockReturnValue({
      url: "ws://127.0.0.1:18789",
      urlSource: "default",
    });
    mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
      auth: {},
      warning: undefined,
    });
    mocks.probeGateway.mockResolvedValue({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "timeout",
      close: null,
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    await scanStatus({ json: true }, {} as never);

    expect(mocks.ensurePluginRegistryLoaded).not.toHaveBeenCalled();
  });

  it("skips plugin compatibility loading for status --json when the config file is missing", async () => {
    mocks.readBestEffortConfig.mockResolvedValue({
      session: {},
      plugins: { enabled: true },
      gateway: {},
    });
    mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
      resolvedConfig: {
        session: {},
        plugins: { enabled: true },
        gateway: {},
      },
      diagnostics: [],
    });
    mocks.getUpdateCheckResult.mockResolvedValue({
      installKind: "git",
      git: null,
      registry: null,
    });
    mocks.getAgentLocalStatuses.mockResolvedValue({
      defaultId: "main",
      agents: [],
    });
    mocks.getStatusSummary.mockResolvedValue({
      linkChannel: undefined,
      sessions: { count: 0, paths: [], defaults: {}, recent: [] },
    });
    mocks.buildGatewayConnectionDetails.mockReturnValue({
      url: "ws://127.0.0.1:18789",
      urlSource: "default",
    });
    mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
      auth: {},
      warning: undefined,
    });
    mocks.probeGateway.mockResolvedValue({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "timeout",
      close: null,
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    await scanStatus({ json: true }, {} as never);

    expect(mocks.buildPluginCompatibilityNotices).not.toHaveBeenCalled();
  });

  it("skips plugin compatibility loading for status --json even with configured channels", async () => {
    mocks.hasPotentialConfiguredChannels.mockReturnValue(true);
    mocks.readBestEffortConfig.mockResolvedValue({
      session: {},
      gateway: {},
      channels: { discord: {} },
    });
    mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
      resolvedConfig: {
        session: {},
        gateway: {},
        channels: { discord: {} },
      },
      diagnostics: [],
    });
    mocks.getUpdateCheckResult.mockResolvedValue({
      installKind: "git",
      git: null,
      registry: null,
    });
    mocks.getAgentLocalStatuses.mockResolvedValue({
      defaultId: "main",
      agents: [],
    });
    mocks.getStatusSummary.mockResolvedValue({
      linkChannel: undefined,
      sessions: { count: 0, paths: [], defaults: {}, recent: [] },
    });
    mocks.buildGatewayConnectionDetails.mockReturnValue({
      url: "ws://127.0.0.1:18789",
      urlSource: "default",
    });
    mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
      auth: {},
      warning: undefined,
    });
    mocks.probeGateway.mockResolvedValue({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "timeout",
      close: null,
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    await scanStatus({ json: true }, {} as never);

    expect(mocks.buildPluginCompatibilityNotices).not.toHaveBeenCalled();
  });

  it("skips gateway and update probes on cold-start status paths", async () => {
    mocks.readBestEffortConfig.mockResolvedValue({
      session: {},
      plugins: { enabled: false },
      gateway: {},
    });
    mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
      resolvedConfig: {
        session: {},
        plugins: { enabled: false },
        gateway: {},
      },
      diagnostics: [],
    });
    mocks.getAgentLocalStatuses.mockResolvedValue({
      defaultId: "main",
      agents: [],
    });
    mocks.getStatusSummary.mockResolvedValue({
      linkChannel: undefined,
      sessions: { count: 0, paths: [], defaults: {}, recent: [] },
    });
    mocks.buildGatewayConnectionDetails.mockReturnValue({
      url: "ws://127.0.0.1:18789",
      urlSource: "default",
    });
    mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
      auth: {},
      warning: undefined,
    });

    await scanStatus({ json: true }, {} as never);
    await scanStatus({ json: false }, {} as never);

    expect(mocks.getUpdateCheckResult).not.toHaveBeenCalled();
    expect(mocks.probeGateway).not.toHaveBeenCalled();
  });

  it("skips memory backend inspection for default memory-core with no existing store", async () => {
    mocks.readBestEffortConfig.mockResolvedValue({
      session: {},
      gateway: {},
    });
    mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
      resolvedConfig: {
        session: {},
        gateway: {},
      },
      diagnostics: [],
    });
    mocks.getUpdateCheckResult.mockResolvedValue({
      installKind: "git",
      git: null,
      registry: null,
    });
    mocks.getAgentLocalStatuses.mockResolvedValue({
      defaultId: "main",
      agents: [],
    });
    mocks.getStatusSummary.mockResolvedValue({
      linkChannel: undefined,
      sessions: { count: 0, paths: [], defaults: {}, recent: [] },
    });
    mocks.buildGatewayConnectionDetails.mockReturnValue({
      url: "ws://127.0.0.1:18789",
      urlSource: "default",
    });
    mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
      auth: {},
      warning: undefined,
    });
    mocks.probeGateway.mockResolvedValue({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "timeout",
      close: null,
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    await scanStatus({ json: true }, {} as never);

    expect(mocks.getMemorySearchManager).not.toHaveBeenCalled();
  });

  it("inspects memory backend when memory search is explicitly configured", async () => {
    mocks.readBestEffortConfig.mockResolvedValue({
      session: {},
      gateway: {},
      agents: {
        defaults: {
          memorySearch: {
            provider: "local",
            local: { modelPath: "/tmp/model.gguf" },
            fallback: "none",
          },
        },
      },
    });
    mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
      resolvedConfig: {
        session: {},
        gateway: {},
        agents: {
          defaults: {
            memorySearch: {
              provider: "local",
              local: { modelPath: "/tmp/model.gguf" },
              fallback: "none",
            },
          },
        },
      },
      diagnostics: [],
    });
    mocks.getUpdateCheckResult.mockResolvedValue({
      installKind: "git",
      git: null,
      registry: null,
    });
    mocks.getAgentLocalStatuses.mockResolvedValue({
      defaultId: "main",
      agents: [],
    });
    mocks.getStatusSummary.mockResolvedValue({
      linkChannel: undefined,
      sessions: { count: 0, paths: [], defaults: {}, recent: [] },
    });
    mocks.buildGatewayConnectionDetails.mockReturnValue({
      url: "ws://127.0.0.1:18789",
      urlSource: "default",
    });
    mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
      auth: {},
      warning: undefined,
    });
    mocks.probeGateway.mockResolvedValue({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "timeout",
      close: null,
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });
    mocks.getMemorySearchManager.mockResolvedValue({
      manager: {
        probeVectorAvailability: vi.fn(async () => true),
        status: vi.fn(() => ({ files: 0, chunks: 0, dirty: false })),
        close: vi.fn(async () => {}),
      },
    });

    await scanStatus({ json: true }, {} as never);

    expect(mocks.getMemorySearchManager).toHaveBeenCalledWith({
      cfg: expect.objectContaining({
        agents: expect.objectContaining({
          defaults: expect.objectContaining({
            memorySearch: expect.any(Object),
          }),
        }),
      }),
      agentId: "main",
      purpose: "status",
    });
  });

  it("preloads configured channel plugins for status --json when channel config exists", async () => {
    mocks.hasPotentialConfiguredChannels.mockReturnValue(true);
    mocks.readBestEffortConfig.mockResolvedValue({
      session: {},
      plugins: { enabled: false },
      gateway: {},
      channels: { telegram: { enabled: false } },
    });
    mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
      resolvedConfig: {
        session: {},
        plugins: { enabled: false },
        gateway: {},
        channels: { telegram: { enabled: false } },
      },
      diagnostics: [],
    });
    mocks.getUpdateCheckResult.mockResolvedValue({
      installKind: "git",
      git: null,
      registry: null,
    });
    mocks.getAgentLocalStatuses.mockResolvedValue({
      defaultId: "main",
      agents: [],
    });
    mocks.getStatusSummary.mockResolvedValue({
      linkChannel: { linked: false },
      sessions: { count: 0, paths: [], defaults: {}, recent: [] },
    });
    mocks.buildGatewayConnectionDetails.mockReturnValue({
      url: "ws://127.0.0.1:18789",
      urlSource: "default",
    });
    mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
      auth: {},
      warning: undefined,
    });
    mocks.probeGateway.mockResolvedValue({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "timeout",
      close: null,
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    await scanStatus({ json: true }, {} as never);

    expect(mocks.ensurePluginRegistryLoaded).toHaveBeenCalledWith({
      scope: "configured-channels",
    });
    // Verify plugin logs were routed to stderr during loading and restored after
    expect(loggingState.forceConsoleToStderr).toBe(false);
    expect(mocks.probeGateway).toHaveBeenCalledWith(
      expect.objectContaining({ detailLevel: "presence" }),
    );
    expect(mocks.callGateway).not.toHaveBeenCalledWith(
      expect.objectContaining({ method: "channels.status" }),
    );
  });

  it("preloads configured channel plugins for status --json when channel auth is env-only", async () => {
    mocks.hasPotentialConfiguredChannels.mockReturnValue(true);
    const prevMatrixToken = process.env.MATRIX_ACCESS_TOKEN;
    process.env.MATRIX_ACCESS_TOKEN = "token";
    mocks.readBestEffortConfig.mockResolvedValue({
      session: {},
      plugins: { enabled: false },
      gateway: {},
    });
    mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
      resolvedConfig: {
        session: {},
        plugins: { enabled: false },
        gateway: {},
      },
      diagnostics: [],
    });
    mocks.getUpdateCheckResult.mockResolvedValue({
      installKind: "git",
      git: null,
      registry: null,
    });
    mocks.getAgentLocalStatuses.mockResolvedValue({
      defaultId: "main",
      agents: [],
    });
    mocks.getStatusSummary.mockResolvedValue({
      linkChannel: { linked: false },
      sessions: { count: 0, paths: [], defaults: {}, recent: [] },
    });
    mocks.buildGatewayConnectionDetails.mockReturnValue({
      url: "ws://127.0.0.1:18789",
      urlSource: "default",
    });
    mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
      auth: {},
      warning: undefined,
    });
    mocks.probeGateway.mockResolvedValue({
      ok: false,
      url: "ws://127.0.0.1:18789",
      connectLatencyMs: null,
      error: "timeout",
      close: null,
      health: null,
      status: null,
      presence: null,
      configSnapshot: null,
    });

    try {
      await scanStatus({ json: true }, {} as never);
    } finally {
      if (prevMatrixToken === undefined) {
        delete process.env.MATRIX_ACCESS_TOKEN;
      } else {
        process.env.MATRIX_ACCESS_TOKEN = prevMatrixToken;
      }
    }

    expect(mocks.ensurePluginRegistryLoaded).toHaveBeenCalledWith({
      scope: "configured-channels",
    });
  });
});
