import { describe, test, expect, afterEach } from "bun:test";
import { existsSync, mkdirSync, lstatSync } from "node:fs";
import { join } from "node:path";
import { syncRemoteRulesRepos } from "@/repo/rules-sync.js";
import { mockLogger } from "../_helpers/mock-logger.js";
import { createFakeGitRepo } from "../_helpers/fake-repo.js";
import type { FakeGitRepo } from "../_helpers/fake-repo.js";

// We'll collect repos to clean up
const cleanups: Array<() => Promise<void>> = [];

afterEach(async () => {
  for (const cleanup of cleanups.splice(0)) {
    await cleanup().catch(() => {});
  }
});

// ── helpers ──────────────────────────────────────────────────────────────────

async function makeRulesRepo(
  mdcFiles: string[] = ["custom.mdc", "general.mdc"],
): Promise<FakeGitRepo> {
  const files: Record<string, string> = {};
  for (const f of mdcFiles) {
    files[`rules/${f}`] = `# ${f} content`;
  }
  // Add a non-mdc file to verify it's NOT symlinked
  files["rules/README.md"] = "not an mdc file";

  const repo = await createFakeGitRepo({ withFiles: files });
  cleanups.push(repo.cleanup);
  return repo;
}

async function makeWorkdir(): Promise<{ workdirPath: string }> {
  const { mkdtempSync } = await import("node:fs");
  const { tmpdir } = await import("node:os");
  const path = mkdtempSync(join(tmpdir(), "workdir-"));
  cleanups.push(async () => {
    const { rmSync } = await import("node:fs");
    rmSync(path, { recursive: true, force: true });
  });
  return { workdirPath: path };
}

function makeRepoConfig(name: string, rulesRepo: FakeGitRepo) {
  return {
    name,
    url: `file://${rulesRepo.path}`, // won't be used as code repo url
    description: "",
    keywords: [],
    remoteRulesRepo: rulesRepo.path, // local path acts as remote for git clone
  };
}

// ── tests ────────────────────────────────────────────────────────────────────

describe("syncRemoteRulesRepos", () => {
  test("no-op when no repos have remoteRulesRepo", async () => {
    const { workdirPath } = await makeWorkdir();
    const logger = mockLogger();
    const repos = [
      { name: "my-api", url: "https://github.com/org/my-api", description: "", keywords: [] },
    ];

    await syncRemoteRulesRepos(repos, workdirPath, logger);

    // No cloning or symlinking should happen
    expect(logger.info).not.toHaveBeenCalled();
  });

  test("clones rules repo and symlinks .mdc files into code repo", async () => {
    const rulesRepo = await makeRulesRepo(["rules.mdc"]);
    const { workdirPath } = await makeWorkdir();
    const logger = mockLogger();

    // Create a code repo directory (simulating it already exists)
    const codeRepoDir = join(workdirPath, "my-api");
    mkdirSync(codeRepoDir, { recursive: true });

    const repos = [makeRepoConfig("my-api", rulesRepo)];

    await syncRemoteRulesRepos(repos, workdirPath, logger);

    // The .mdc file should be symlinked
    const symlinkPath = join(workdirPath, "my-api", ".cursor", "rules", "rules.mdc");
    expect(existsSync(symlinkPath)).toBe(true);
    expect(lstatSync(symlinkPath).isSymbolicLink()).toBe(true);
  });

  test("non-.mdc files in rules repo are NOT symlinked", async () => {
    const rulesRepo = await makeRulesRepo(["rules.mdc"]);
    const { workdirPath } = await makeWorkdir();
    const logger = mockLogger();

    const codeRepoDir = join(workdirPath, "my-api");
    mkdirSync(codeRepoDir, { recursive: true });

    const repos = [makeRepoConfig("my-api", rulesRepo)];

    await syncRemoteRulesRepos(repos, workdirPath, logger);

    // README.md should not be symlinked
    const readmePath = join(workdirPath, "my-api", ".cursor", "rules", "README.md");
    expect(existsSync(readmePath)).toBe(false);
  });

  test("symlinks are refreshed on second sync (pull path)", async () => {
    const rulesRepo = await makeRulesRepo(["rules.mdc"]);
    const { workdirPath } = await makeWorkdir();
    const logger = mockLogger();

    const codeRepoDir = join(workdirPath, "my-api");
    mkdirSync(codeRepoDir, { recursive: true });

    const repos = [makeRepoConfig("my-api", rulesRepo)];

    // First sync — should clone
    await syncRemoteRulesRepos(repos, workdirPath, logger);

    // Second sync — should pull and re-symlink
    await syncRemoteRulesRepos(repos, workdirPath, logger);

    const symlinkPath = join(workdirPath, "my-api", ".cursor", "rules", "rules.mdc");
    expect(existsSync(symlinkPath)).toBe(true);
    expect(lstatSync(symlinkPath).isSymbolicLink()).toBe(true);
  });

  test("same rules repo is symlinked into multiple code repos", async () => {
    const rulesRepo = await makeRulesRepo(["shared.mdc"]);
    const { workdirPath } = await makeWorkdir();
    const logger = mockLogger();

    mkdirSync(join(workdirPath, "repo-a"), { recursive: true });
    mkdirSync(join(workdirPath, "repo-b"), { recursive: true });

    const repoA = { ...makeRepoConfig("repo-a", rulesRepo) };
    const repoB = { ...makeRepoConfig("repo-b", rulesRepo) };
    // Both share the same rules repo URL
    repoB.remoteRulesRepo = repoA.remoteRulesRepo;

    await syncRemoteRulesRepos([repoA, repoB], workdirPath, logger);

    const symlinkA = join(workdirPath, "repo-a", ".cursor", "rules", "shared.mdc");
    const symlinkB = join(workdirPath, "repo-b", ".cursor", "rules", "shared.mdc");
    expect(existsSync(symlinkA)).toBe(true);
    expect(existsSync(symlinkB)).toBe(true);
  });

  test("warns and continues when rules repo is unreachable", async () => {
    const { workdirPath } = await makeWorkdir();
    const logger = mockLogger();

    const repos = [
      {
        name: "my-api",
        url: "https://github.com/org/my-api",
        description: "",
        keywords: [],
        remoteRulesRepo: "/nonexistent/path/to/rules-repo",
      },
    ];

    // Should not throw
    await expect(syncRemoteRulesRepos(repos, workdirPath, logger)).resolves.toBeUndefined();
    expect(logger.warn).toHaveBeenCalled();
  });

  test("warns when rules dir does not exist in cloned repo", async () => {
    // Create a repo without a 'rules' directory
    const emptyRepo = await createFakeGitRepo({ withFiles: { "README.md": "# empty" } });
    cleanups.push(emptyRepo.cleanup);
    const { workdirPath } = await makeWorkdir();
    const logger = mockLogger();

    mkdirSync(join(workdirPath, "my-api"), { recursive: true });

    const repos = [
      {
        name: "my-api",
        url: "https://github.com/org/my-api",
        description: "",
        keywords: [],
        remoteRulesRepo: emptyRepo.path,
      },
    ];

    await syncRemoteRulesRepos(repos, workdirPath, logger);

    // Should warn about missing rules dir
    expect(logger.warn).toHaveBeenCalled();
  });

  test("warns when rules dir has no .mdc files", async () => {
    // Create a repo with rules dir but no mdc files
    const repo = await createFakeGitRepo({
      withFiles: { "rules/README.md": "# no mdc files here" },
    });
    cleanups.push(repo.cleanup);
    const { workdirPath } = await makeWorkdir();
    const logger = mockLogger();

    mkdirSync(join(workdirPath, "my-api"), { recursive: true });

    const repos = [
      {
        name: "my-api",
        url: "https://github.com/org/my-api",
        description: "",
        keywords: [],
        remoteRulesRepo: repo.path,
      },
    ];

    await syncRemoteRulesRepos(repos, workdirPath, logger);

    expect(logger.warn).toHaveBeenCalled();
  });
});
