import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { audit } from '../audit.js'

let baseDir: string
let moduleDir: string
let projectPath: string

beforeEach(() => {
  baseDir = mkdtempSync(join(tmpdir(), 'audit-dev-tests-'))
  moduleDir = join(baseDir, 'ba', 'CRM', 'PIPELINE')
  projectPath = join(baseDir, 'project')
  mkdirSync(moduleDir, { recursive: true })
  mkdirSync(projectPath, { recursive: true })
})

afterEach(() => {
  rmSync(baseDir, { recursive: true, force: true })
})

/** Write a use-case.md with the given ACs under <moduleDir>/<section>/. */
function writeUseCaseMd(section: string, ucCode: string, title: string, acs: Array<{ id: string; text: string }>): void {
  const sectionDir = join(moduleDir, section)
  mkdirSync(sectionDir, { recursive: true })
  const md = [
    `<!-- ba:use-case level=section code=${section} -->`,
    `# Cas d'usage`,
    ``,
    `### ${ucCode} — ${title}`,
    `- **Acteur principal** : X`,
    `- **Acceptance Criteria** :`,
    ...acs.map(a => `  - [ ] ${a.id} — ${a.text}`),
    ``,
  ].join('\n')
  writeFileSync(join(sectionDir, 'use-case.md'), md, 'utf8')
}

/** Write a .cs test file under <projectPath>/Tests/Acceptance/ with the given traits + optional TODOs. */
function writeTestCs(filename: string, traits: string[], todos: string[] = []): void {
  const dir = join(projectPath, 'Tests', 'Acceptance')
  mkdirSync(dir, { recursive: true })
  const factBlocks = traits.map((t, idx) => `    [Fact]\n    [Trait("AC", "${t}")]\n    public async Task TestX${idx}()\n    {\n        ${todos[idx] ?? ''}\n        await Task.CompletedTask;\n    }`).join('\n\n')
  const content = [
    `namespace MyApp.Tests.Pipeline.Acceptance;`,
    ``,
    `public class GeneratedTests`,
    `{`,
    factBlocks,
    `}`,
    ``,
  ].join('\n')
  writeFileSync(join(dir, filename), content, 'utf8')
}

describe('audit-dev-tests — DEV-TEST-001 (BA AC → test trait)', () => {
  it('passes when every BA AC has a matching [Trait]', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'Créer', [
      { id: 'AC-01', text: 'POST returns 201.' },
      { id: 'AC-02', text: 'POST returns 400 on negative.' },
    ])
    writeTestCs('OpportunitesAcceptanceTests.cs', [
      'UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01',
      'UC-CRM-PIPELINE-OPPORTUNITES-001#AC-02',
    ])
    const report = await audit({ moduleDir, projectPath })
    const t001 = report.findings.find(f => f.rule === 'DEV-TEST-001')!
    expect(t001.severity).toBe('ok')
    expect(report.success).toBe(true)
  })

  it('fails (err) when a BA AC is missing from the tests', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'Créer', [
      { id: 'AC-01', text: 'POST returns 201.' },
      { id: 'AC-02', text: 'POST returns 400.' },
    ])
    writeTestCs('OpportunitesAcceptanceTests.cs', ['UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01'])
    const report = await audit({ moduleDir, projectPath })
    const t001 = report.findings.find(f => f.rule === 'DEV-TEST-001')!
    expect(t001.severity).toBe('err')
    expect(t001.refs).toEqual(['UC-CRM-PIPELINE-OPPORTUNITES-001#AC-02'])
    expect(report.success).toBe(false)
  })

  it('lists ALL missing ACs, sorted', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'Créer', [
      { id: 'AC-03', text: 'X' },
      { id: 'AC-01', text: 'Y' },
      { id: 'AC-02', text: 'Z' },
    ])
    writeTestCs('Empty.cs', [])
    const report = await audit({ moduleDir, projectPath })
    const t001 = report.findings.find(f => f.rule === 'DEV-TEST-001')!
    expect(t001.refs).toEqual([
      'UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01',
      'UC-CRM-PIPELINE-OPPORTUNITES-001#AC-02',
      'UC-CRM-PIPELINE-OPPORTUNITES-001#AC-03',
    ])
  })
})

describe('audit-dev-tests — DEV-TEST-002 (no TODO[AC- marker remains)', () => {
  it('passes when no TODO marker is found', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'X', [{ id: 'AC-01', text: 'Y' }])
    writeTestCs('OpportunitesAcceptanceTests.cs', ['UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01'])
    const report = await audit({ moduleDir, projectPath })
    const t002 = report.findings.find(f => f.rule === 'DEV-TEST-002')!
    expect(t002.severity).toBe('ok')
  })

  it('fails (err) when a TODO[AC-NN] marker is left in a test body', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'X', [{ id: 'AC-01', text: 'Y' }])
    writeTestCs(
      'OpportunitesAcceptanceTests.cs',
      ['UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01'],
      ['// TODO[AC-01]: implement assertion'],
    )
    const report = await audit({ moduleDir, projectPath })
    const t002 = report.findings.find(f => f.rule === 'DEV-TEST-002')!
    expect(t002.severity).toBe('err')
    expect(t002.refs).toContain('Tests/Acceptance/OpportunitesAcceptanceTests.cs')
    expect(report.todoMarkerCount).toBe(1)
    expect(report.success).toBe(false)
  })

  it('counts multiple TODO markers across multiple files', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'X', [
      { id: 'AC-01', text: 'A' },
      { id: 'AC-02', text: 'B' },
    ])
    writeTestCs(
      'A.cs',
      ['UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01'],
      ['// TODO[AC-01]: assert'],
    )
    writeTestCs(
      'B.cs',
      ['UC-CRM-PIPELINE-OPPORTUNITES-001#AC-02'],
      ['// TODO[AC-02]: assert'],
    )
    const report = await audit({ moduleDir, projectPath })
    expect(report.todoMarkerCount).toBe(2)
    const t002 = report.findings.find(f => f.rule === 'DEV-TEST-002')!
    expect(t002.refs.length).toBe(2)
  })
})

describe('audit-dev-tests — DEV-TEST-003 (no stale [Trait])', () => {
  it('passes when every test trait matches a BA AC', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'X', [{ id: 'AC-01', text: 'Y' }])
    writeTestCs('OpportunitesAcceptanceTests.cs', ['UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01'])
    const report = await audit({ moduleDir, projectPath })
    const t003 = report.findings.find(f => f.rule === 'DEV-TEST-003')!
    expect(t003.severity).toBe('ok')
  })

  it('warns (not err) on stale trait that points to a deleted BA AC', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'X', [{ id: 'AC-01', text: 'Y' }])
    writeTestCs('OpportunitesAcceptanceTests.cs', [
      'UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01',
      'UC-CRM-PIPELINE-OPPORTUNITES-001#AC-99', // stale
    ])
    const report = await audit({ moduleDir, projectPath })
    const t003 = report.findings.find(f => f.rule === 'DEV-TEST-003')!
    expect(t003.severity).toBe('warn')
    expect(t003.refs).toEqual(['UC-CRM-PIPELINE-OPPORTUNITES-001#AC-99'])
    // err comes from missing AC-99 in tests? no — AC-99 isn't in BA. So no DEV-TEST-001 err.
    // success is still true because DEV-TEST-003 is warn only.
    expect(report.success).toBe(true)
  })
})

describe('audit-dev-tests — combined / reporting', () => {
  it('reports baAcCount and testTraitCount accurately', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'X', [
      { id: 'AC-01', text: 'A' },
      { id: 'AC-02', text: 'B' },
    ])
    writeUseCaseMd('clients', 'UC-CRM-PIPELINE-CLIENTS-001', 'Y', [{ id: 'AC-01', text: 'C' }])
    writeTestCs('A.cs', [
      'UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01',
      'UC-CRM-PIPELINE-OPPORTUNITES-001#AC-02',
      'UC-CRM-PIPELINE-CLIENTS-001#AC-01',
    ])
    const report = await audit({ moduleDir, projectPath })
    expect(report.baAcCount).toBe(3)
    expect(report.testTraitCount).toBe(3)
    expect(report.success).toBe(true)
  })

  it('returns success=false when ANY rule is err, even if others are ok', async () => {
    writeUseCaseMd('opportunites', 'UC-CRM-PIPELINE-OPPORTUNITES-001', 'X', [
      { id: 'AC-01', text: 'A' },
      { id: 'AC-02', text: 'B' },
    ])
    writeTestCs(
      'A.cs',
      ['UC-CRM-PIPELINE-OPPORTUNITES-001#AC-01'],
      ['// TODO[AC-01]: assert'],
    )
    const report = await audit({ moduleDir, projectPath })
    expect(report.findings.find(f => f.rule === 'DEV-TEST-001')!.severity).toBe('err') // missing AC-02
    expect(report.findings.find(f => f.rule === 'DEV-TEST-002')!.severity).toBe('err') // TODO marker
    expect(report.success).toBe(false)
  })

  it('handles a module with zero ACs gracefully (no rules emit ok or err)', async () => {
    // No use-case.md written at all
    writeTestCs('Empty.cs', [])
    const report = await audit({ moduleDir, projectPath })
    expect(report.baAcCount).toBe(0)
    expect(report.testTraitCount).toBe(0)
    // DEV-TEST-001 and DEV-TEST-003 only emit when there is something to compare;
    // DEV-TEST-002 always emits.
    const codes = report.findings.map(f => f.rule)
    expect(codes).toContain('DEV-TEST-002')
    expect(report.success).toBe(true)
  })
})
