import { describe, expect, it } from "vitest";
import {
  GITHUB_RELEASE_BODY_MAX_BYTES,
  GITHUB_RELEASE_BODY_MAX_CHARACTERS,
  extractChangelogSection,
  formatContributionRecordProvenance,
  formatShippedBaselineExclusions,
  parseContributionRecordProvenance,
  parseShippedBaselineExclusions,
  releaseNotesVersionForTag,
  renderGithubReleaseNotes,
  verifyGithubReleaseNotes,
} from "../../scripts/render-github-release-notes.mts";

const repository = "openclaw/openclaw";
const tag = "v2026.7.1-beta.3";
const version = "2026.7.1";

function changelogFor(record: string): string {
  return [
    "# Changelog",
    "",
    `## ${version}`,
    "",
    "### Highlights",
    "",
    "- A grouped user-facing highlight.",
    "",
    "### Fixes",
    "",
    "- A grouped user-facing fix.",
    "",
    "### Complete contribution record",
    "",
    record,
    "",
    "## 2026.6.11",
    "",
    "- Previous release.",
    "",
  ].join("\n");
}

describe("GitHub release-note rendering", () => {
  it("round-trips canonical contribution provenance and accepts published legacy lines", () => {
    const target = "a".repeat(40);
    const singular = formatContributionRecordProvenance({
      base: "v2026.7.2-beta.7",
      target,
      inRangePullRequests: 1,
      retainedSeedOnlyPullRequests: 0,
      uniquePullRequests: 1,
    });
    const commaSeparated = formatContributionRecordProvenance({
      base: "v2026.7.2-beta.7",
      target,
      inRangePullRequests: 1_234,
      retainedSeedOnlyPullRequests: 56,
      uniquePullRequests: 1_290,
    });

    expect(singular).toContain("1 in-range PR + 0 retained seed-only PRs = 1 unique PR.");
    expect(commaSeparated).toContain(
      "1,234 in-range PRs + 56 retained seed-only PRs = 1,290 unique PRs.",
    );
    expect(
      parseContributionRecordProvenance(
        [singular, "", "#### Pull requests", "", "- **PR #123** fix: canonical example."].join(
          "\n",
        ),
      ),
    ).toEqual({
      base: "v2026.7.2-beta.7",
      target,
      inRangePullRequests: 1,
      retainedSeedOnlyPullRequests: 0,
      uniquePullRequests: 1,
    });
    const legacy = parseContributionRecordProvenance(
      [
        `This audited record covers the complete v2026.7.2-beta.6..02d06caeb0febe7ec3c0df1454b85c38f3fb27d1 history: 1 merged PR. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.`,
        "",
        "#### Pull requests",
        "",
        "- **PR #123** fix: legacy example.",
      ].join("\n"),
    );
    expect(legacy).toMatchObject({ uniquePullRequests: 1 });
    expect(() => formatContributionRecordProvenance(legacy!)).toThrow("requires split PR counts");
    expect(() =>
      parseContributionRecordProvenance(
        commaSeparated.replace("= 1,290 unique PRs", "= 1,291 unique PRs"),
      ),
    ).toThrow("provenance arithmetic is invalid");
    expect(() =>
      parseContributionRecordProvenance(commaSeparated.replace("1,234", "1234")),
    ).toThrow("provenance is malformed");
    expect(() =>
      parseContributionRecordProvenance(singular.replace("1 in-range", "001 in-range")),
    ).toThrow("provenance is malformed");
    expect(() => parseContributionRecordProvenance(singular)).toThrow(
      "positive contribution record requires a Pull requests section",
    );
  });

  it("emits the complete matching section including its version heading when it fits", () => {
    const rendered = renderGithubReleaseNotes({
      changelog: changelogFor("- **PR #123** fix: example. Thanks @contributor."),
      version,
      tag,
      repository,
    });

    expect(rendered.mode).toBe("full");
    expect(rendered.body).toBe(
      [
        `## ${version}`,
        "",
        "### Highlights",
        "",
        "- A grouped user-facing highlight.",
        "",
        "### Fixes",
        "",
        "- A grouped user-facing fix.",
        "",
        "### Complete contribution record",
        "",
        "- **PR #123** fix: example. Thanks @contributor.",
      ].join("\n"),
    );
  });

  it("replaces an oversized contribution record with a tag-pinned link", () => {
    const oversizedRecord = `- **PR #123** ${"record-only-detail ".repeat(9_000)}`;
    const rendered = renderGithubReleaseNotes({
      changelog: changelogFor(oversizedRecord),
      version,
      tag,
      repository,
    });

    expect(rendered.mode).toBe("compact");
    expect(rendered.body).toContain(`## ${version}\n\n### Highlights`);
    expect(rendered.body).toContain("- A grouped user-facing fix.");
    expect(rendered.body).toContain("### Complete contribution record");
    expect(rendered.body).toContain(
      "https://github.com/openclaw/openclaw/blob/v2026.7.1-beta.3/CHANGELOG.md#complete-contribution-record",
    );
    expect(rendered.body).not.toContain("record-only-detail");
    expect(rendered.size.characters).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_CHARACTERS);
    expect(rendered.size.bytes).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_BYTES);
  });

  it("keeps a fitting full section and omits only a proof tail that would overflow", () => {
    const nearlyFullRecord = `- **PR #123** ${"x".repeat(124_500)}`;
    const changelog = changelogFor(nearlyFullRecord);
    const withoutProof = renderGithubReleaseNotes({
      changelog,
      version,
      tag,
      repository,
    });
    const withProof = renderGithubReleaseNotes({
      changelog,
      version,
      tag,
      repository,
      verification: `### Release verification\n\n- proof: ${"y".repeat(1_000)}`,
    });

    expect(withoutProof.mode).toBe("full");
    expect(withProof.mode).toBe("full");
    expect(withProof.verificationIncluded).toBe(false);
    expect(withProof.verificationOmitted).toBe(true);
    expect(withProof.body).toBe(withoutProof.body);
    expect(withProof.body).not.toContain("### Release verification");
    expect(withProof.size.characters).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_CHARACTERS);
    expect(withProof.size.bytes).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_BYTES);
  });

  it("uses the full form at exactly 125,000 bytes and compacts at 125,001", () => {
    const seed = renderGithubReleaseNotes({
      changelog: changelogFor("x"),
      version,
      tag,
      repository,
    });
    const exactRecordLength =
      GITHUB_RELEASE_BODY_MAX_BYTES - seed.size.bytes + Buffer.byteLength("x");
    const exact = renderGithubReleaseNotes({
      changelog: changelogFor("x".repeat(exactRecordLength)),
      version,
      tag,
      repository,
    });
    const over = renderGithubReleaseNotes({
      changelog: changelogFor("x".repeat(exactRecordLength + 1)),
      version,
      tag,
      repository,
    });

    expect(exact.mode).toBe("full");
    expect(exact.size).toEqual({
      characters: GITHUB_RELEASE_BODY_MAX_CHARACTERS,
      bytes: GITHUB_RELEASE_BODY_MAX_BYTES,
    });
    expect(over.mode).toBe("compact");
  });

  it("compacts when multibyte text exceeds the byte limit before the character limit", () => {
    const rendered = renderGithubReleaseNotes({
      changelog: changelogFor("é".repeat(63_000)),
      version,
      tag,
      repository,
    });

    expect(rendered.mode).toBe("compact");
    expect(rendered.size.bytes).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_BYTES);
    expect(rendered.size.characters).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_CHARACTERS);
  });

  it("normalizes correction tags to the stable changelog section", () => {
    expect(releaseNotesVersionForTag("v2026.7.1-2")).toBe("2026.7.1");
    const rendered = renderGithubReleaseNotes({
      changelog: changelogFor("- **PR #123** fix: correction."),
      version,
      tag: "v2026.7.1-2",
      repository,
    });

    expect(rendered.body).toContain("## 2026.7.1");
  });

  it("prefers a correction tag's dedicated changelog section when one exists", () => {
    const changelog = [
      "# Changelog",
      "",
      "## 2026.7.1-2",
      "",
      "- Correction-only fix.",
      "",
      `## ${version}`,
      "",
      "- Stable release notes.",
      "",
    ].join("\n");
    const rendered = renderGithubReleaseNotes({
      changelog,
      version,
      tag: "v2026.7.1-2",
      repository,
    });

    expect(rendered.body).toContain("## 2026.7.1-2");
    expect(rendered.body).toContain("Correction-only fix.");
    expect(rendered.body).not.toContain("Stable release notes.");
  });

  it("round-trips canonical shipped baseline exclusions and rejects malformed metadata", () => {
    const line = formatShippedBaselineExclusions([
      { ref: "v2026.6.11", count: 2, pullRequests: [108, 101] },
      { ref: "v2026.6.10-beta.2", count: 0, pullRequests: [] },
    ]);

    expect(line).toBe(
      "Shipped baseline exclusions: v2026.6.10-beta.2 (0 PRs); v2026.6.11 (2 PRs: #101, #108).",
    );
    expect(parseShippedBaselineExclusions(line)).toEqual([
      { ref: "v2026.6.10-beta.2", count: 0, pullRequests: [] },
      { ref: "v2026.6.11", count: 2, pullRequests: [101, 108] },
    ]);
    expect(() =>
      parseShippedBaselineExclusions("Shipped baseline exclusion: v2026.6.11 (8 PRs)."),
    ).toThrow("malformed shipped baseline exclusion");
    expect(() =>
      parseShippedBaselineExclusions("Shipped baseline exclusions: v2026.6.11 (2 PRs: #101)."),
    ).toThrow("invalid shipped baseline exclusion count");
  });

  it("rejects tag/version drift and legacy oversized ledger anchors", () => {
    expect(() =>
      renderGithubReleaseNotes({
        changelog: changelogFor("- **PR #123** fix: example."),
        version,
        tag: "v2026.7.2-beta.1",
        repository,
      }),
    ).toThrow("requires CHANGELOG.md version 2026.7.2");

    expect(() =>
      renderGithubReleaseNotes({
        changelog: changelogFor(
          `### Complete contribution ledger\n\n${"legacy ".repeat(20_000)}`,
        ).replace("### Complete contribution record\n\n", ""),
        version,
        tag,
        repository,
      }),
    ).toThrow("cannot be compacted without a complete contribution record");
  });

  it("prefers an alpha tag's exact changelog heading over the Unreleased fallback", () => {
    // Shipped alpha tags carry their own heading with no base section,
    // matching the tagged CHANGELOG.md shape of v2026.6.20-alpha.1.
    const changelog = [
      "# Changelog",
      "",
      "## 2026.7.1-alpha.2",
      "",
      "- Alpha-only fix.",
      "",
      "## 2026.6.11",
      "",
      "- Previous release.",
      "",
    ].join("\n");
    const rendered = renderGithubReleaseNotes({
      changelog,
      version,
      tag: "v2026.7.1-alpha.2",
      repository,
    });

    expect(rendered.body).toContain("## 2026.7.1-alpha.2");
    expect(rendered.body).toContain("Alpha-only fix.");
    expect(rendered.body).not.toContain("Previous release.");
  });

  it("permits the Unreleased fallback only for alpha tags", () => {
    const changelog = changelogFor("- **PR #123** fix: example.").replace(
      "## 2026.7.1",
      "## Unreleased",
    );
    const rendered = renderGithubReleaseNotes({
      changelog,
      version,
      tag: "v2026.7.1-alpha.1",
      repository,
    });

    expect(rendered.body).toContain("## 2026.7.1");
    expect(rendered.body).not.toContain("## Unreleased");
    expect(() =>
      renderGithubReleaseNotes({
        changelog,
        version,
        tag,
        repository,
      }),
    ).toThrow("CHANGELOG.md does not contain ## 2026.7.1");
    expect(() =>
      renderGithubReleaseNotes({
        changelog,
        version: "foo",
        tag: "vfoo-alpha.1",
        repository,
      }),
    ).toThrow("invalid release tag");
  });

  it("ignores fenced pseudo-headings and handles a release heading at EOF", () => {
    const fenced = [
      `## ${version}`,
      "",
      "```md",
      "## 2099.1.1",
      "```",
      "",
      "### Fixes",
      "",
      "- Still in the current release.",
      "",
      "## 2026.6.11",
    ].join("\n");

    expect(extractChangelogSection(fenced, version)).toContain("- Still in the current release.");
    expect(extractChangelogSection(`## ${version}`, version)).toBe(`## ${version}`);
  });

  it("compacts at the real contribution record instead of a fenced pseudo-heading", () => {
    const changelog = changelogFor(`- **PR #123** ${"record-only-detail ".repeat(9_000)}`).replace(
      "### Fixes",
      ["```md", "### Complete contribution record", "```", "", "### Fixes"].join("\n"),
    );
    const rendered = renderGithubReleaseNotes({ changelog, version, tag, repository });

    expect(rendered.mode).toBe("compact");
    expect(rendered.body).toContain("```md\n### Complete contribution record\n```");
    expect(rendered.body).toContain("- A grouped user-facing fix.");
    expect(rendered.body).not.toContain("record-only-detail");
  });

  it("verifies the exact generated compact body and optional proof tail", () => {
    const changelog = changelogFor(`- **PR #123** ${"z".repeat(130_000)}`);
    const verification = "### Release verification\n\n- release SHA: `abc123`";
    const rendered = renderGithubReleaseNotes({
      changelog,
      version,
      tag,
      repository,
      verification,
    });

    expect(
      verifyGithubReleaseNotes({
        body: rendered.body,
        changelog,
        version,
        tag,
        repository,
      }),
    ).toMatchObject({ matches: true, mode: "compact" });
    expect(
      verifyGithubReleaseNotes({
        body: rendered.body.replace("tag-pinned", "mutable"),
        changelog,
        version,
        tag,
        repository,
      }).matches,
    ).toBe(false);
    expect(
      verifyGithubReleaseNotes({
        body: rendered.body,
        changelog,
        version,
        tag: "v2026.7.1-beta.2",
        repository,
      }).matches,
    ).toBe(false);
  });

  it("does not treat fenced verification headings as appended proof", () => {
    const changelog = changelogFor(
      [
        "```md",
        "### Release verification",
        "",
        "- Example only.",
        "```",
        "",
        "- **PR #123** fix: example.",
      ].join("\n"),
    );
    const rendered = renderGithubReleaseNotes({ changelog, version, tag, repository });

    expect(
      verifyGithubReleaseNotes({
        body: rendered.body,
        changelog,
        version,
        tag,
        repository,
      }),
    ).toMatchObject({ matches: true, verificationIncluded: false });
  });
});
