import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';

import { parseChangelog, resolveChangelogGroupKey } from './changelog-parse';

const SAMPLE = `# Changelog

## v0.19.2 (2026-08-04)

### Changed
- Subsync: picks both tracks now.

### Fixed
- Overlay: shows the plain line immediately.

<details>
<summary>Internal changes</summary>

### Internal
- Patched \`undici\`.

</details>

## v0.19.1 (2026-08-01)

### Added
- **Word Card Type:**
  - Adds a setting.
  - Flags clear each other.

## v0.18.0 (2026-07-01)

### Fixed
- Something older.
`;

test('changelog parser reads versions, dates, and sections in file order', () => {
  const entries = parseChangelog(SAMPLE);

  assert.deepEqual(
    entries.map((entry) => `${entry.version}@${entry.date}`),
    ['0.19.2@2026-08-04', '0.19.1@2026-08-01', '0.18.0@2026-07-01'],
  );
  assert.deepEqual(
    entries[0]?.sections.map((section) => section.heading),
    ['Changed', 'Fixed', 'Internal'],
  );
  assert.deepEqual(entries[0]?.sections[1]?.items, [
    { text: 'Overlay: shows the plain line immediately.', children: [] },
  ]);
});

test('changelog parser flags sections inside the details block as internal', () => {
  const entries = parseChangelog(SAMPLE);
  const sections = entries[0]?.sections ?? [];

  assert.deepEqual(
    sections.map((section) => section.internal),
    [false, false, true],
  );
  assert.deepEqual(sections[2]?.items, [{ text: 'Patched `undici`.', children: [] }]);
});

test('changelog parser groups entries by major.minor', () => {
  const entries = parseChangelog(SAMPLE);

  assert.deepEqual(
    entries.map((entry) => entry.groupKey),
    ['0.19', '0.19', '0.18'],
  );
  assert.equal(resolveChangelogGroupKey('1.2.3'), '1.2');
});

test('changelog parser keeps bullets that precede any section heading', () => {
  const entries = parseChangelog('## v0.1.0 (2025-01-01)\n\n- Initial release.\n');

  assert.deepEqual(entries[0]?.sections, [
    {
      heading: 'Changes',
      items: [{ text: 'Initial release.', children: [] }],
      internal: false,
    },
  ]);
});

test('changelog parser drops empty sections and tolerates missing dates', () => {
  const entries = parseChangelog('## v0.2.0\n\n### Added\n\n### Fixed\n- One fix.\n');

  assert.equal(entries[0]?.date, '');
  assert.deepEqual(
    entries[0]?.sections.map((section) => section.heading),
    ['Fixed'],
  );
});

test('changelog parser keeps indented sub-bullets nested under their lead bullet', () => {
  const entries = parseChangelog(SAMPLE);
  const added = entries[1]?.sections.find((section) => section.heading === 'Added');

  assert.deepEqual(added?.items, [
    {
      text: '**Word Card Type:**',
      children: [
        { text: 'Adds a setting.', children: [] },
        { text: 'Flags clear each other.', children: [] },
      ],
    },
  ]);
});

test('changelog parser nests three bullet levels and rejoins wrapped lines', () => {
  const entries = parseChangelog(
    [
      '## v0.9.0 (2025-05-05)',
      '',
      '### Added',
      '- Top level',
      '  - Second level',
      '    - Third level',
      '      continued on the next line',
      '  - Back to second level',
      '- Another top level',
      '',
    ].join('\n'),
  );

  assert.deepEqual(entries[0]?.sections[0]?.items, [
    {
      text: 'Top level',
      children: [
        {
          text: 'Second level',
          children: [{ text: 'Third level continued on the next line', children: [] }],
        },
        { text: 'Back to second level', children: [] },
      ],
    },
    { text: 'Another top level', children: [] },
  ]);
});

test('changelog parser reads prerelease and build metadata version headings', () => {
  const entries = parseChangelog(
    [
      '## v0.16.0 (2026-06-01)',
      '',
      '### Added',
      '- New in 0.16.',
      '',
      '## v0.15.0-rc.1+build.2 (2026-05-29)',
      '',
      '### Added',
      '- Release candidate note.',
      '',
    ].join('\n'),
  );

  // The prerelease heading has to become its own entry. Asserting the exact
  // version list is what catches the failure mode: a heading the regex misses
  // is not skipped, its notes silently fold into the release above it.
  assert.deepEqual(
    entries.map((entry) => entry.version),
    ['0.16.0', '0.15.0-rc.1+build.2'],
  );
  assert.equal(entries[1]?.date, '2026-05-29');
  assert.equal(entries[1]?.groupKey, '0.15');
  assert.equal(entries[0]?.sections.length, 1);
  // The prerelease body has to land on its own entry, not fold into 0.16.0.
  assert.deepEqual(entries[1]?.sections, [
    {
      heading: 'Added',
      items: [{ text: 'Release candidate note.', children: [] }],
      internal: false,
    },
  ]);
});

test('changelog parser handles the repo CHANGELOG.md', () => {
  const markdown = fs.readFileSync(path.join(process.cwd(), 'CHANGELOG.md'), 'utf8');
  const entries = parseChangelog(markdown);

  assert.ok(entries.length > 3);
  for (const entry of entries) {
    assert.match(entry.version, /^\d+\.\d+\.\d+/);
    assert.ok(entry.sections.length > 0, `expected sections for v${entry.version}`);
    for (const section of entry.sections) {
      for (const item of section.items) {
        assert.ok(item.text.length > 0, `empty bullet in v${entry.version}`);
      }
    }
  }

  // Older entries group notes under a bold lead bullet; nesting must survive.
  const breaking = entries
    .find((entry) => entry.version === '0.15.0')
    ?.sections.find((section) => section.heading === 'Breaking Changes');
  assert.deepEqual(
    breaking?.items.map((item) => `${item.text}:${item.children.length}`),
    ['**Subsync:**:2', '**N+1 Highlighting:**:2'],
  );
});
