import { describe, test, expect, mock, spyOn, beforeEach, afterEach } from "bun:test";
import { GitHubClientImpl } from "@/github/client.js";
import { collectGithubMergedPrs } from "@/analytics/github-metrics.js";
import { mockLogger } from "../_helpers/mock-logger.js";
import type { RepoConfig } from "@/types/index.js";

// Use spyOn instead of mock.module so the mock is scoped to this file and
// does not leak into test/github/client.test.ts or test/server/routes/debug.test.ts
// when running in parallel (Bun's mock.module is global to the process).

let listMergedPrsSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
  listMergedPrsSpy = spyOn(GitHubClientImpl.prototype, "listMergedPrs").mockResolvedValue(
    [] as never,
  );
});

afterEach(() => {
  mock.restore();
});

// ── types ─────────────────────────────────────────────────────────────────────

interface MergedPrRecord {
  mergedAt: string;
  authorLogin: string;
  headRef: string;
}

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

function makeRepo(name: string, url = `https://github.com/org/${name}`): RepoConfig {
  return {
    name,
    url,
    description: "",
    keywords: [],
    defaultBranch: "main",
  } as RepoConfig;
}

function makeMergedPr(
  overrides: {
    mergedAt?: string;
    authorLogin?: string;
    headRef?: string;
  } = {},
): MergedPrRecord {
  return {
    mergedAt: overrides.mergedAt ?? "2026-04-08T12:00:00Z",
    authorLogin: overrides.authorLogin ?? "human-dev",
    headRef: overrides.headRef ?? "chore/no-ticket",
  };
}

// Two-week bucket fixture: week 0 = Apr 7–14, week 1 = Apr 14–21
const TWO_WEEK_BUCKETS = [
  { weekStart: "2026-04-07", weekEnd: "2026-04-14" },
  { weekStart: "2026-04-14", weekEnd: "2026-04-21" },
];

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

describe("collectGithubMergedPrs", () => {
  test("empty buckets returns empty result without calling GitHub", async () => {
    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: [],
      logger: mockLogger(),
    });
    expect(result).toEqual({ weekly: [], byRepo: [], bySquad: [] });
    expect(listMergedPrsSpy).not.toHaveBeenCalled();
  });

  test("single repo, no merged PRs → all zeros", async () => {
    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    expect(result.weekly[0]?.prsMergedTotal).toBe(0);
    expect(result.weekly[0]?.prsMergedBot).toBe(0);
    expect(result.byRepo).toHaveLength(1);
    expect(result.byRepo[0]?.prsMergedTotal).toBe(0);
  });

  test("bot-authored PR is counted in prsMergedBot", async () => {
    const pr = makeMergedPr({ authorLogin: "hu-agent[bot]", mergedAt: "2026-04-08T10:00:00Z" });
    listMergedPrsSpy.mockResolvedValue([pr] as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    expect(result.weekly[0]?.prsMergedTotal).toBe(1);
    expect(result.weekly[0]?.prsMergedBot).toBe(1);
    expect(result.weekly[0]?.prsMergedOtherBots).toBe(0);
  });

  test("human PR is not counted as bot", async () => {
    const pr = makeMergedPr({ authorLogin: "human-dev", mergedAt: "2026-04-08T10:00:00Z" });
    listMergedPrsSpy.mockResolvedValue([pr] as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    expect(result.weekly[0]?.prsMergedTotal).toBe(1);
    expect(result.weekly[0]?.prsMergedBot).toBe(0);
    expect(result.weekly[0]?.prsMergedOtherBots).toBe(0);
  });

  test("other-bot PR (login ends [bot]) is counted in prsMergedOtherBots", async () => {
    const pr = makeMergedPr({
      authorLogin: "github-actions[bot]",
      mergedAt: "2026-04-08T10:00:00Z",
    });
    listMergedPrsSpy.mockResolvedValue([pr] as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    expect(result.weekly[0]?.prsMergedOtherBots).toBe(1);
    expect(result.weekly[0]?.prsMergedBot).toBe(0);
  });

  test("bot login comparison is case-insensitive", async () => {
    const pr = makeMergedPr({ authorLogin: "Hu-Agent[bot]", mergedAt: "2026-04-08T10:00:00Z" });
    listMergedPrsSpy.mockResolvedValue([pr] as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    expect(result.weekly[0]?.prsMergedBot).toBe(1);
    expect(result.weekly[0]?.prsMergedOtherBots).toBe(0);
  });

  test("PR merged before first bucket is not counted", async () => {
    const pr = makeMergedPr({ mergedAt: "2026-04-01T10:00:00Z" }); // before 2026-04-07
    listMergedPrsSpy.mockResolvedValue([pr] as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    expect(result.weekly[0]?.prsMergedTotal).toBe(0);
    expect(result.weekly[1]?.prsMergedTotal).toBe(0);
  });

  test("PR merged after last bucket is not counted", async () => {
    const pr = makeMergedPr({ mergedAt: "2026-04-25T10:00:00Z" }); // after 2026-04-21
    listMergedPrsSpy.mockResolvedValue([pr] as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    expect(result.weekly[0]?.prsMergedTotal).toBe(0);
    expect(result.weekly[1]?.prsMergedTotal).toBe(0);
  });

  test("PR branch with ticket prefix creates squad entry with correct tribe", async () => {
    const pr = makeMergedPr({ headRef: "fix/SQDP-123-the-bug", mergedAt: "2026-04-08T10:00:00Z" });
    listMergedPrsSpy.mockResolvedValue([pr] as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    const squadRow = result.bySquad.find((r) => r.squad === "SQDP");
    expect(squadRow).toBeDefined();
    expect(squadRow!.prsMergedTotal).toBe(1);
    expect(typeof squadRow!.tribe).toBe("string");
    expect(squadRow!.tribe.length).toBeGreaterThan(0);
  });

  test("PR branch with no ticket prefix → squad 'unmapped' with empty tribe", async () => {
    const pr = makeMergedPr({ headRef: "chore/cleanup", mergedAt: "2026-04-08T10:00:00Z" });
    listMergedPrsSpy.mockResolvedValue([pr] as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    const unmapped = result.bySquad.find((r) => r.squad === "unmapped");
    expect(unmapped).toBeDefined();
    expect(unmapped!.tribe).toBe("");
  });

  test("single repo failure is skipped — other repos still processed", async () => {
    const goodPr = makeMergedPr({ mergedAt: "2026-04-08T10:00:00Z" });
    let callCount = 0;
    listMergedPrsSpy.mockImplementation(async () => {
      callCount++;
      if (callCount === 1) throw new Error("API error for repo-a");
      return [goodPr] as never;
    });

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("repo-a"), makeRepo("repo-b")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    expect(result.byRepo).toHaveLength(2);
    expect(result.byRepo[0]?.prsMergedTotal).toBe(0); // repo-a
    expect(result.byRepo[1]?.prsMergedTotal).toBe(1); // repo-b
    expect(result.weekly[0]?.prsMergedTotal).toBe(1);
    // 15 s timeout: REPO_DELAY_MS = 5_000 sleep fires once (2 repos, 1 gap).
  }, 15_000);

  test("bySquad is sorted descending by prsMergedTotal", async () => {
    const prs = [
      makeMergedPr({ headRef: "fix/SQDP-1", mergedAt: "2026-04-08T10:00:00Z" }),
      makeMergedPr({ headRef: "fix/SQDP-2", mergedAt: "2026-04-08T11:00:00Z" }),
      makeMergedPr({ headRef: "fix/SQRN-1", mergedAt: "2026-04-08T12:00:00Z" }),
    ];
    listMergedPrsSpy.mockResolvedValue(prs as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("my-api")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });
    const totals = result.bySquad.map((r) => r.prsMergedTotal);
    for (let i = 1; i < totals.length; i++) {
      expect(totals[i - 1]!).toBeGreaterThanOrEqual(totals[i]!);
    }
  });

  test("REPO_DELAY_MS sleep is not triggered for a single repo", async () => {
    // The production loop only sleeps `if (!first)`, so a single-repo call
    // never fires the inter-repo delay. Assert via call-count (not wall-clock).
    const pr = makeMergedPr({ mergedAt: "2026-04-08T10:00:00Z" });
    listMergedPrsSpy.mockResolvedValue([pr] as never);

    const result = await collectGithubMergedPrs({
      repos: [makeRepo("only-repo")],
      getGitHubToken: async () => "tok",
      botLogin: "hu-agent[bot]",
      buckets: TWO_WEEK_BUCKETS,
      logger: mockLogger(),
    });

    expect(listMergedPrsSpy).toHaveBeenCalledTimes(1);
    expect(result.byRepo).toHaveLength(1);
    expect(result.weekly[0]?.prsMergedTotal).toBe(1);
  });
});
