import { describe, expect, it } from "vitest";
import {
  contributionRecordFor,
  ledgerChecks,
  ledgerFor,
  renderContributionRecordEntry,
} from "../../.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs";

const targetSha = "a".repeat(40);

function contributionLedger({
  nodes,
  seededPullRequests = [],
  sourcePullRequests = [],
  sourceReferences = [],
}: {
  nodes: Map<number, Record<string, unknown>>;
  seededPullRequests?: number[];
  sourcePullRequests?: number[];
  sourceReferences?: number[];
}) {
  return ledgerFor(
    "v2026.7.2-beta.7",
    targetSha,
    [...nodes.keys()],
    nodes,
    new Map(),
    new Map(),
    { issuesByPullRequest: new Map() },
    {
      legacyIssues: new Map(),
      pullRequests: new Map(
        seededPullRequests.map((number) => [
          number,
          { externalReferences: [], references: [], thanks: [] },
        ]),
      ),
    },
    new Set(sourcePullRequests),
    sourceReferences,
    [],
    [],
    new Set(),
    [],
    Date.parse("2026-08-05T00:00:00Z"),
  ) as ReturnType<typeof ledgerFor> & {
    provenance: {
      inRangePullRequests: number;
      retainedSeedOnlyPullRequests: number;
      uniquePullRequests: number;
    };
  };
}

describe("renderContributionRecordEntry", () => {
  it("keeps external and linked issue references without repeating PR title references", () => {
    expect(
      renderContributionRecordEntry({
        number: 123,
        title: "Fix local openclaw/openclaw#45 and openclaw/imsg#141",
        linkedIssues: [{ number: 45 }, { number: 67 }],
        thanks: ["alice", "bob"],
      }),
    ).toBe("- **PR #123** Related #45, openclaw/imsg#141, #67. Thanks @alice and @bob.");
  });

  it("deduplicates resolved issues and retains seeded cross-repository references", () => {
    expect(
      renderContributionRecordEntry({
        number: 124,
        title: "Fix #45, #45, and OpenClaw/imsg#141",
        externalReferences: ["openclaw/imsg#141"],
        priorReferences: [67],
        linkedIssues: [{ number: 45 }, { number: 67 }],
        thanks: [],
      }),
    ).toBe("- **PR #124** Related #45, OpenClaw/imsg#141, #67.");
  });

  it("renders every source PR even without issue references or credits", () => {
    expect(
      renderContributionRecordEntry({
        number: 456,
        title: "Internal cleanup",
        linkedIssues: [],
        thanks: [],
      }),
    ).toBe("- **PR #456**");
  });

  it("retains references and credits when a compact record is seeded again", () => {
    const line = "- **PR #125** Related #45, openclaw/imsg#141. Thanks @alice and @bob.";
    const record = contributionRecordFor({
      source: [
        "## 2026.7.1",
        "",
        "### Complete contribution record",
        "",
        "#### Pull requests",
        "",
        line,
      ].join("\n"),
    });
    const seeded = record.pullRequests.get(125);

    expect(seeded).toEqual({
      externalReferences: ["openclaw/imsg#141"],
      references: [45],
      thanks: ["alice", "bob"],
    });
    expect(
      renderContributionRecordEntry({
        number: 125,
        title: "Title changed after release",
        priorReferences: seeded?.references,
        externalReferences: seeded?.externalReferences,
        linkedIssues: [{ number: 45 }],
        thanks: seeded?.thanks ?? [],
      }),
    ).toBe(line);
  });

  it("retains seeded credits when the production ledger is rebuilt", () => {
    const priorRecord = contributionRecordFor({
      source: [
        "## 2026.7.1",
        "",
        "### Complete contribution record",
        "",
        "#### Pull requests",
        "",
        "- **PR #125** Thanks @alice and @bob.",
      ].join("\n"),
    });
    const nodes = new Map([
      [
        125,
        {
          __typename: "PullRequest",
          author: { __typename: "User", login: "carol" },
          closingIssuesReferences: { nodes: [] },
          mergedAt: "2026-07-08T00:00:00Z",
          title: "fix: keep release credits",
        },
      ],
    ]);

    const result = ledgerFor(
      "v2026.6.11",
      targetSha,
      [125],
      nodes,
      new Map(),
      new Map(),
      { issuesByPullRequest: new Map() },
      priorRecord,
      new Set([125]),
      new Set(),
      new Set(),
      new Set(),
      new Set(),
      [],
      Date.parse("2026-07-09T00:00:00Z"),
    );

    expect(result.ledger).toContain("- **PR #125** Thanks @carol and @alice and @bob.");
  });

  it("counts associated and PR-typed source refs before retained seed-only rows", () => {
    const nodes = new Map(
      [1, 2, 3].map((number) => [
        number,
        {
          __typename: "PullRequest",
          closingIssuesReferences: { nodes: [] },
          mergedAt: "2026-08-04T00:00:00Z",
          title: `fix: contribution ${number}`,
        },
      ]),
    );
    const result = contributionLedger({
      nodes,
      seededPullRequests: [1, 3],
      sourcePullRequests: [1],
      sourceReferences: [2],
    });

    expect(result.provenance).toEqual({
      inRangePullRequests: 2,
      retainedSeedOnlyPullRequests: 1,
      uniquePullRequests: 3,
    });
    expect(result.ledger).toContain("2 in-range PRs + 1 retained seed-only PR = 3 unique PRs.");
  });

  it("reports zero retained seed-only PRs when every row is in range", () => {
    const nodes = new Map([
      [
        1,
        {
          __typename: "PullRequest",
          closingIssuesReferences: { nodes: [] },
          mergedAt: "2026-08-04T00:00:00Z",
          title: "fix: in-range contribution",
        },
      ],
    ]);
    const result = contributionLedger({ nodes, sourcePullRequests: [1] });

    expect(result.provenance).toMatchObject({
      inRangePullRequests: 1,
      retainedSeedOnlyPullRequests: 0,
      uniquePullRequests: 1,
    });
  });

  it("reports all rows as retained seed-only when the release range has no PRs", () => {
    const nodes = new Map(
      [1, 2].map((number) => [
        number,
        {
          __typename: "PullRequest",
          closingIssuesReferences: { nodes: [] },
          mergedAt: "2026-08-04T00:00:00Z",
          title: `fix: seeded contribution ${number}`,
        },
      ]),
    );
    const result = contributionLedger({ nodes, seededPullRequests: [1, 2] });

    expect(result.provenance).toMatchObject({
      inRangePullRequests: 0,
      retainedSeedOnlyPullRequests: 2,
      uniquePullRequests: 2,
    });
  });

  it("rejects a forged canonical range and seed partition", () => {
    const source = [
      "## 2026.7.1",
      "",
      "### Highlights",
      "",
      "- Highlight one.",
      "- Highlight two.",
      "- Highlight three.",
      "- Highlight four.",
      "- Highlight five.",
      "",
      "### Changes",
      "",
      "### Fixes",
      "",
      "### Complete contribution record",
      "",
      `This audited record covers the complete base..${targetSha} history: 0 in-range PRs + 1 retained seed-only PR = 1 unique PR.`,
      "",
      "#### Pull requests",
      "",
      "- **PR #456**",
    ].join("\n");
    const entry = {
      number: 456,
      title: "fix: example",
      editorialEligible: true,
      priorReferences: [],
      externalReferences: [],
      linkedIssues: [],
      thanks: [],
    };

    expect(
      ledgerChecks(
        {
          source,
          expectedProvenance: {
            inRangePullRequests: 1,
            retainedSeedOnlyPullRequests: 0,
            uniquePullRequests: 1,
          },
        },
        [entry],
        new Map([[456, { __typename: "PullRequest" }]]),
        [],
      ),
    ).toContain("contribution record provenance partition does not match generated inventory");
  });

  it("retains references from a verbose record when the source title changes", () => {
    const record = contributionRecordFor({
      source: [
        "## 2026.7.1",
        "",
        "### Complete contribution record",
        "",
        "#### Pull requests",
        "",
        "- **PR #126** Fix #46 and openclaw/imsg#142. Related #68. Thanks @alice.",
      ].join("\n"),
    });
    const seeded = record.pullRequests.get(126);

    expect(seeded).toEqual({
      externalReferences: ["openclaw/imsg#142"],
      references: [46, 68],
      thanks: ["alice"],
    });
  });

  it("requires complete resolved issue tokens rather than matching substrings", () => {
    const source = [
      "## 2026.7.1",
      "",
      "### Highlights",
      "",
      "- Highlight one.",
      "- Highlight two.",
      "- Highlight three.",
      "- Highlight four.",
      "- Highlight five.",
      "",
      "### Changes",
      "",
      "### Fixes",
      "",
      "### Complete contribution record",
      "",
      `This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
      "",
      "#### Pull requests",
      "",
      "- **PR #456** Related openclaw/imsg#141.",
    ].join("\n");
    const entry = {
      number: 456,
      title: "Internal cleanup",
      editorialEligible: false,
      priorReferences: [],
      externalReferences: [],
      linkedIssues: [{ number: 45 }, { number: 141 }],
      thanks: [],
    };

    expect(
      ledgerChecks({ source }, [entry], new Map([[456, { __typename: "PullRequest" }]]), []),
    ).toEqual([
      "missing #45 on contribution record for PR #456",
      "missing #141 on contribution record for PR #456",
    ]);
  });

  it("does not require an out-of-range PR referenced by a PR title", () => {
    const line = "- **PR #456**";
    const source = [
      "## 2026.7.1",
      "",
      "### Highlights",
      "",
      "- Highlight one.",
      "- Highlight two.",
      "- Highlight three.",
      "- Highlight four.",
      "- Highlight five.",
      "",
      "### Changes",
      "",
      "### Fixes",
      "",
      "### Complete contribution record",
      "",
      `This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
      "",
      "#### Pull requests",
      "",
      line,
    ].join("\n");
    const entry = {
      number: 456,
      title: "docs: align backup rules (#123)",
      editorialEligible: false,
      priorReferences: [123],
      externalReferences: [],
      linkedIssues: [],
      thanks: [],
    };

    expect(renderContributionRecordEntry(entry)).toBe(line);
    expect(
      ledgerChecks({ source }, [entry], new Map([[456, { __typename: "PullRequest" }]]), []),
    ).toEqual([]);
  });

  it("accepts case-only differences in cross-repository references", () => {
    const line = "- **PR #127** Related OpenClaw/imsg#143.";
    const source = [
      "## 2026.7.1",
      "",
      "### Highlights",
      "",
      "- Highlight one.",
      "- Highlight two.",
      "- Highlight three.",
      "- Highlight four.",
      "- Highlight five.",
      "",
      "### Changes",
      "",
      "### Fixes",
      "",
      "### Complete contribution record",
      "",
      `This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
      "",
      "#### Pull requests",
      "",
      line,
    ].join("\n");
    const entry = {
      number: 127,
      title: "Internal cleanup",
      editorialEligible: false,
      priorReferences: [],
      externalReferences: ["openclaw/imsg#143"],
      linkedIssues: [],
      thanks: [],
    };

    expect(
      ledgerChecks({ source }, [entry], new Map([[127, { __typename: "PullRequest" }]]), []),
    ).toEqual([]);
  });
});
