import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { checkVersionAndAiFilesStaleness } from "./updates.js";
import { getVersion } from "./versionApi.js";
import { logMessage } from "../../bundler/log.js";
import { checkAiFilesStalenessAndLog } from "./aiFiles/index.js";
import { readProjectConfig } from "./config.js";
import type { Context } from "../../bundler/context.js";

vi.mock("./versionApi.js", () => ({
  getVersion: vi.fn(),
}));

vi.mock("./aiFiles/index.js", () => ({
  checkAiFilesStalenessAndLog: vi.fn(),
  isAiFilesDisabled: vi.fn((aiFilesConfig) =>
    aiFilesConfig?.enabled !== undefined
      ? aiFilesConfig.enabled === false
      : aiFilesConfig?.disableStalenessMessage === true,
  ),
}));

vi.mock("../../bundler/log.js", () => ({
  logMessage: vi.fn(),
}));

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

const mockGetVersion = vi.mocked(getVersion);
const mockLogMessage = vi.mocked(logMessage);
const mockCheckAiFilesStalenessAndLog = vi.mocked(checkAiFilesStalenessAndLog);
const mockReadProjectConfig = vi.mocked(readProjectConfig);

const fakeCtx = {} as Context;

const okVersion = (overrides?: object) => ({
  kind: "ok" as const,
  data: {
    message: null,
    guidelinesHash: "abc-guidelines-hash",
    agentSkillsSha: "abc-skills-sha",
    disableSkillsCli: false,
    disableSkillsCliMessage: null,
    ...overrides,
  },
});

describe("checkVersionAndAiFilesStaleness", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockCheckAiFilesStalenessAndLog.mockResolvedValue(undefined);
    mockReadProjectConfig.mockResolvedValue({
      projectConfig: {
        functions: "convex",
        node: { externalPackages: [] },
        generateCommonJSApi: false,
        codegen: { staticApi: true, staticDataModel: true },
      },
      configPath: "convex.json",
    });
  });

  afterEach(() => {
    vi.resetAllMocks();
  });

  it("logs version message when server provides one", async () => {
    mockGetVersion.mockResolvedValue(
      okVersion({ message: "New version available: 1.2.3" }),
    );

    await checkVersionAndAiFilesStaleness(fakeCtx);

    expect(mockLogMessage).toHaveBeenCalledWith("New version available: 1.2.3");
  });

  it("does not log when version has no message", async () => {
    mockGetVersion.mockResolvedValue(okVersion());

    await checkVersionAndAiFilesStaleness(fakeCtx);

    expect(mockLogMessage).not.toHaveBeenCalled();
  });

  it("does nothing when getVersion returns error", async () => {
    mockGetVersion.mockResolvedValue({ kind: "error" });

    await checkVersionAndAiFilesStaleness(fakeCtx);

    expect(mockLogMessage).not.toHaveBeenCalled();
    expect(mockCheckAiFilesStalenessAndLog).not.toHaveBeenCalled();
  });

  it("passes hashes and project paths to staleness check", async () => {
    mockGetVersion.mockResolvedValue(
      okVersion({
        guidelinesHash:
          "02e43fc1ff0ee48db8da468f5c7525877d8056fcd56c77d78a166ac447efb91c",
        agentSkillsSha: "abc123def456abc123def456abc123def456abc1",
      }),
    );

    await checkVersionAndAiFilesStaleness(fakeCtx);

    expect(mockCheckAiFilesStalenessAndLog).toHaveBeenCalledWith({
      canonicalGuidelinesHash:
        "02e43fc1ff0ee48db8da468f5c7525877d8056fcd56c77d78a166ac447efb91c",
      canonicalAgentSkillsSha: "abc123def456abc123def456abc123def456abc1",
      projectDir: expect.any(String),
      convexDir: expect.any(String),
    });
  });

  it("passes null hashes when version has none", async () => {
    mockGetVersion.mockResolvedValue(
      okVersion({ guidelinesHash: null, agentSkillsSha: null }),
    );

    await checkVersionAndAiFilesStaleness(fakeCtx);

    expect(mockCheckAiFilesStalenessAndLog).toHaveBeenCalledWith({
      canonicalGuidelinesHash: null,
      canonicalAgentSkillsSha: null,
      projectDir: expect.any(String),
      convexDir: expect.any(String),
    });
  });

  it("skips staleness check when aiFiles.disableStalenessMessage is true", async () => {
    mockGetVersion.mockResolvedValue(okVersion());
    mockReadProjectConfig.mockResolvedValue({
      projectConfig: {
        functions: "convex",
        node: { externalPackages: [] },
        generateCommonJSApi: false,
        codegen: { staticApi: true, staticDataModel: true },
        aiFiles: { disableStalenessMessage: true },
      },
      configPath: "convex.json",
    });

    await checkVersionAndAiFilesStaleness(fakeCtx);

    expect(mockCheckAiFilesStalenessAndLog).not.toHaveBeenCalled();
  });

  it("silently skips staleness check when project config cannot be resolved", async () => {
    mockGetVersion.mockResolvedValue(okVersion());
    mockReadProjectConfig.mockRejectedValue(new Error("no convex.json"));

    await expect(
      checkVersionAndAiFilesStaleness(fakeCtx),
    ).resolves.toBeUndefined();
    expect(mockCheckAiFilesStalenessAndLog).not.toHaveBeenCalled();
  });
});
