/**
 * clean.test.ts — Rename + delete passes on synthetic markdown content,
 * plus an end-to-end fixture exercising applyPlan().
 */
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtemp, rm, mkdir, writeFile, readFile } from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import {
  renameCodesInText,
  removeHeadingBlocks,
  scrubBodyReferences,
  applyPlan,
  sectionCodeToFolder,
} from '../clean.js';
import type { RenameChange, DeletionChange, SectionKey } from '../types.js';

// ---------------------------------------------------------------------------
// renameCodesInText
// ---------------------------------------------------------------------------

describe('renameCodesInText', () => {
  it('renames a UC heading code', () => {
    const before = '### UC-CRM-PIPELINE-OPPORTUNITES-001 — Créer\n';
    const after = renameCodesInText(before, 'CRM', 'PIPELINE', 'OPPORTUNITES', 'PROSPECTS');
    expect(after).toBe('### UC-CRM-PIPELINE-PROSPECTS-001 — Créer\n');
  });

  it('renames SCR and BR body cites in the same text', () => {
    const before =
      '- **Écrans liés** : SCR-CRM-PIPELINE-OPPORTUNITES-001\n' +
      '- **Règles liées** : BR-CRM-PIPELINE-OPPORTUNITES-003\n';
    const after = renameCodesInText(before, 'CRM', 'PIPELINE', 'OPPORTUNITES', 'PROSPECTS');
    expect(after).toContain('SCR-CRM-PIPELINE-PROSPECTS-001');
    expect(after).toContain('BR-CRM-PIPELINE-PROSPECTS-003');
  });

  it('leaves untouched codes from other sections', () => {
    const before = '### UC-CRM-PIPELINE-CLIENTS-001 — Listing\n';
    const after = renameCodesInText(before, 'CRM', 'PIPELINE', 'OPPORTUNITES', 'PROSPECTS');
    expect(after).toBe(before);
  });

  it('is idempotent (running twice gives the same result)', () => {
    const before = '### UC-CRM-PIPELINE-OPPORTUNITES-001 — Créer\n';
    const once = renameCodesInText(before, 'CRM', 'PIPELINE', 'OPPORTUNITES', 'PROSPECTS');
    const twice = renameCodesInText(once, 'CRM', 'PIPELINE', 'OPPORTUNITES', 'PROSPECTS');
    expect(twice).toBe(once);
  });
});

// ---------------------------------------------------------------------------
// removeHeadingBlocks
// ---------------------------------------------------------------------------

describe('removeHeadingBlocks', () => {
  it('drops a single UC block ending at the next ### heading', () => {
    const before = [
      '### UC-CRM-PIPELINE-LEGACY-001 — Old',
      '- field A',
      '- field B',
      '',
      '### UC-CRM-PIPELINE-OPPORTUNITES-001 — Keep me',
      '- field C',
      '',
    ].join('\n');
    const after = removeHeadingBlocks(before, 'CRM', 'PIPELINE', 'LEGACY');
    expect(after).not.toContain('UC-CRM-PIPELINE-LEGACY-001');
    expect(after).toContain('UC-CRM-PIPELINE-OPPORTUNITES-001');
  });

  it('drops a block ending at EOF when no further heading follows', () => {
    const before = [
      '### UC-CRM-PIPELINE-OPPORTUNITES-001 — Keep',
      '- field A',
      '',
      '### UC-CRM-PIPELINE-LEGACY-001 — Drop',
      '- field B',
    ].join('\n');
    const after = removeHeadingBlocks(before, 'CRM', 'PIPELINE', 'LEGACY');
    expect(after).not.toContain('LEGACY');
    expect(after).toContain('OPPORTUNITES-001');
  });

  it('drops every block matching the deleted section', () => {
    const before = [
      '### UC-CRM-PIPELINE-LEGACY-001 — A',
      '',
      '### UC-CRM-PIPELINE-LEGACY-002 — B',
      '',
      '### UC-CRM-PIPELINE-LEGACY-003 — C',
      '',
    ].join('\n');
    const after = removeHeadingBlocks(before, 'CRM', 'PIPELINE', 'LEGACY');
    expect(after).not.toContain('LEGACY');
  });

  it('stops at a higher-level ## heading too', () => {
    const before = [
      '### UC-CRM-PIPELINE-LEGACY-001 — A',
      '- step',
      '',
      '## Other section',
      'Text',
    ].join('\n');
    const after = removeHeadingBlocks(before, 'CRM', 'PIPELINE', 'LEGACY');
    expect(after).not.toContain('LEGACY-001');
    expect(after).toContain('## Other section');
  });
});

// ---------------------------------------------------------------------------
// scrubBodyReferences
// ---------------------------------------------------------------------------

describe('scrubBodyReferences', () => {
  it('removes a standalone code reference in a bullet and collapses to —', () => {
    const before = '- **Règles liées** : BR-CRM-PIPELINE-LEGACY-003\n';
    const after = scrubBodyReferences(before, 'CRM', 'PIPELINE', 'LEGACY');
    expect(after).toContain('—');
    expect(after).not.toContain('LEGACY');
  });

  it('keeps surviving entries in a comma-separated list', () => {
    const before =
      '- **Règles liées** : BR-CRM-PIPELINE-OPPORTUNITES-001, BR-CRM-PIPELINE-LEGACY-003\n';
    const after = scrubBodyReferences(before, 'CRM', 'PIPELINE', 'LEGACY');
    expect(after).toContain('BR-CRM-PIPELINE-OPPORTUNITES-001');
    expect(after).not.toContain('LEGACY');
  });
});

// ---------------------------------------------------------------------------
// sectionCodeToFolder
// ---------------------------------------------------------------------------

describe('sectionCodeToFolder', () => {
  it('inverts uppercase + underscore → kebab', () => {
    expect(sectionCodeToFolder('EXCHANGE_HISTORY')).toBe('exchange-history');
    expect(sectionCodeToFolder('OPPORTUNITES')).toBe('opportunites');
  });
});

// ---------------------------------------------------------------------------
// applyPlan (integration on a temp tree)
// ---------------------------------------------------------------------------

describe('applyPlan (integration)', () => {
  let tmp: string;

  beforeEach(async () => {
    tmp = await mkdtemp(path.join(os.tmpdir(), 'reconcile-apply-'));
    // Tree: CRM/PIPELINE/{opportunites (existing), clients (existing)}
    // Files reference an extra LEGACY section that has been deleted from the menu.
    await mkdir(path.join(tmp, 'CRM', 'PIPELINE', 'opportunites'), { recursive: true });
    await mkdir(path.join(tmp, 'CRM', 'PIPELINE', 'clients'), { recursive: true });
    await writeFile(path.join(tmp, 'CRM', 'index.md'), '<!-- app -->\n');
    await writeFile(path.join(tmp, 'CRM', 'PIPELINE', 'index.md'), '<!-- module -->\n');
    await writeFile(
      path.join(tmp, 'CRM', 'PIPELINE', 'opportunites', 'index.md'),
      '<!-- section -->\n',
    );
    await writeFile(
      path.join(tmp, 'CRM', 'PIPELINE', 'clients', 'index.md'),
      '<!-- section -->\n',
    );
    await writeFile(
      path.join(tmp, 'CRM', 'PIPELINE', 'opportunites', 'use-case.md'),
      [
        '<!-- ba:use-case level=section code=opportunites -->',
        '# Cas d\'usage — CRM / PIPELINE / opportunites',
        '',
        '### UC-CRM-PIPELINE-OPPORTUNITES-001 — Créer',
        '- **Règles liées** : BR-CRM-PIPELINE-OPPORTUNITES-001, BR-CRM-PIPELINE-LEGACY-003',
        '',
      ].join('\n'),
    );
    await writeFile(
      path.join(tmp, 'CRM', 'PIPELINE', 'règles-métier.md'),
      [
        '### BR-CRM-PIPELINE-OPPORTUNITES-001 — Live rule',
        '- field A',
        '',
        '### BR-CRM-PIPELINE-LEGACY-003 — Stale rule',
        '- field B',
        '',
      ].join('\n'),
    );
  });

  afterEach(async () => {
    await rm(tmp, { recursive: true, force: true });
  });

  it('applies a deletion: removes stale heading + scrubs body refs', async () => {
    const deletions: DeletionChange[] = [
      {
        kind: 'section',
        path: 'CRM/PIPELINE/LEGACY' as SectionKey,
        codeCount: 1,
        reason: 'no-rename-candidate',
      },
    ];
    const result = await applyPlan(tmp, [], deletions);
    expect(result.filesModified.length).toBeGreaterThan(0);

    const ruleContent = await readFile(
      path.join(tmp, 'CRM', 'PIPELINE', 'règles-métier.md'),
      'utf-8',
    );
    expect(ruleContent).not.toContain('LEGACY');
    expect(ruleContent).toContain('OPPORTUNITES-001');

    const ucContent = await readFile(
      path.join(tmp, 'CRM', 'PIPELINE', 'opportunites', 'use-case.md'),
      'utf-8',
    );
    expect(ucContent).not.toContain('LEGACY');
    expect(ucContent).toContain('BR-CRM-PIPELINE-OPPORTUNITES-001');
  });

  it('applies a rename: rewrites every matching code', async () => {
    // Tweak fixture: rename CLIENTS code → PROSPECTS arrival exists.
    await writeFile(
      path.join(tmp, 'CRM', 'PIPELINE', 'clients', 'use-case.md'),
      '### UC-CRM-PIPELINE-CLIENTS-001 — Listing\n',
    );
    // Pretend the menu now uses "prospects" instead of "clients":
    await mkdir(path.join(tmp, 'CRM', 'PIPELINE', 'prospects'), { recursive: true });
    await writeFile(
      path.join(tmp, 'CRM', 'PIPELINE', 'prospects', 'index.md'),
      '<!-- section -->\n',
    );

    const renames: RenameChange[] = [
      {
        kind: 'section',
        from: 'CRM/PIPELINE/CLIENTS' as SectionKey,
        to: 'CRM/PIPELINE/PROSPECTS' as SectionKey,
        confidence: 0.85,
        reason: 'code-similar',
      },
    ];
    await applyPlan(tmp, renames, []);
    const renamed = await readFile(
      path.join(tmp, 'CRM', 'PIPELINE', 'clients', 'use-case.md'),
      'utf-8',
    );
    expect(renamed).toContain('UC-CRM-PIPELINE-PROSPECTS-001');
    expect(renamed).not.toContain('UC-CRM-PIPELINE-CLIENTS-001');
  });

  it('is idempotent (running deletion twice changes nothing the second time)', async () => {
    const deletions: DeletionChange[] = [
      {
        kind: 'section',
        path: 'CRM/PIPELINE/LEGACY' as SectionKey,
        codeCount: 1,
        reason: 'no-rename-candidate',
      },
    ];
    await applyPlan(tmp, [], deletions);
    const second = await applyPlan(tmp, [], deletions);
    expect(second.filesModified).toHaveLength(0);
  });
});
