import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { audit } from '../audit.js'
import type { ActionAlignmentArgs } from '../types.js'

// ─── Fixtures ────────────────────────────────────────────────────────────

const PAGESPEC = `<!-- pagespec -->
\`\`\`json
{
  "screenCode": "SCR-APP-CAT-TYPEAFFAIRE-001",
  "appCode": "app",
  "module": "referentiels",
  "section": "types-affaire",
  "entity": "TypeAffaire",
  "view": "list",
  "permission": "referentiels.types-affaire.read",
  "actions": [
    { "code": "create", "scope": "header", "labelKey": "list.create", "permission": "referentiels.types-affaire.create", "variant": "primary" },
    { "code": "syncFromPce", "kind": "api", "scope": "header", "endpoint": "sync-from-proconcept", "httpMethod": "POST", "labelKey": "list.actions.syncFromPce", "permission": "referentiels.types-affaire.execute", "ucReference": "UC-APP-CAT-TYPEAFFAIRE-007" },
    { "code": "analyzeImpact", "kind": "api", "scope": "header", "endpoint": "impact", "httpMethod": "GET", "labelKey": "list.actions.analyzeImpact", "permission": "referentiels.types-affaire.read", "ucReference": "UC-APP-CAT-TYPEAFFAIRE-009" },
    { "code": "open", "kind": "navigate", "scope": "row", "targetScreen": "SCR-APP-CAT-TYPEAFFAIRE-002", "targetRoute": "routes.referentiels.detail(item.id)", "labelKey": "list.actions.open", "permission": "referentiels.types-affaire.read" }
  ]
}
\`\`\`

Some human prose follows.
`

const CONTROLLER_CS = `using Microsoft.AspNetCore.Mvc;
namespace TestApp.Api.Controllers.Catalog;

[ApiController]
[Route("api/referentiels/types-affaire")]
public class TypesAffaireController : ControllerBase
{
    private readonly ITypeAffaireService _service;
    public TypesAffaireController(ITypeAffaireService service) => _service = service;

    [HttpGet]
    public async Task<ActionResult> GetAll() { return Ok(); }

    [HttpGet("{id:guid}")]
    public async Task<ActionResult> GetById(Guid id) { return Ok(); }

    [HttpPost("sync-from-proconcept")]
    public async Task<ActionResult> SyncFromProconcept(CancellationToken ct) { return NoContent(); }

    [HttpGet("impact")]
    public async Task<ActionResult> Impact() { return Ok(); }
}
`

/** Legacy service.ts: calls /sync-from-pce instead of the pagespec's /sync-from-proconcept. */
const SERVICE_TS_DRIFT = `import { apiClient } from '@/services/apiClient'
const API_PATH = '/api/referentiels/types-affaire'

export const typeAffaireService = {
  getAll: async () => apiClient.get(\`\${API_PATH}\`),
  syncFromPce: async () => { await apiClient.post(\`\${API_PATH}/sync-from-pce\`) },
  analyzeImpact: async () => { await apiClient.post(\`\${API_PATH}/analyze-impact\`) },
}
`

/** Aligned service.ts: matches the pagespec verbatim. */
const SERVICE_TS_ALIGNED = `import { apiClient } from '@/services/apiClient'
const API_PATH = '/api/referentiels/types-affaire'

export const typeAffaireService = {
  getAll: async () => apiClient.get(\`\${API_PATH}\`),
  syncFromProconcept: async () => { await apiClient.post(\`\${API_PATH}/sync-from-proconcept\`) },
  impact: async () => { await apiClient.get(\`\${API_PATH}/impact\`) },
}
`

// ─── Test scaffolding ────────────────────────────────────────────────────

let projectDir: string
let backendDir: string
let moduleDir: string

function writeFixtures(serviceContent: string) {
  // Frontend
  const servicesDir = path.join(projectDir, 'src/features/referentiels/typeAffaire/services')
  mkdirSync(servicesDir, { recursive: true })
  writeFileSync(path.join(servicesDir, 'typeAffaireService.ts'), serviceContent)

  // Backend
  const controllersDir = path.join(backendDir, 'src/TestApp.Api/Controllers/Catalog')
  mkdirSync(controllersDir, { recursive: true })
  writeFileSync(path.join(controllersDir, 'TypesAffaireController.cs'), CONTROLLER_CS)

  // Pagespec
  const pagespecsDir = path.join(moduleDir, 'pagespecs')
  mkdirSync(pagespecsDir, { recursive: true })
  writeFileSync(path.join(pagespecsDir, 'TypeAffaire.list.md'), PAGESPEC)
}

function makeArgs(): ActionAlignmentArgs {
  return {
    projectPath: projectDir,
    backendPath: backendDir,
    modulePath: moduleDir,
    mode: 'report-only',
    sourceOfTruth: 'pagespec',
    writeReport: false,
  }
}

beforeAll(() => {
  const root = mkdtempSync(path.join(tmpdir(), 'ssaction-'))
  projectDir = path.join(root, 'project')
  backendDir = path.join(root, 'backend')
  moduleDir = path.join(root, 'ba/APP/CATALOG')
  mkdirSync(projectDir, { recursive: true })
  mkdirSync(backendDir, { recursive: true })
  mkdirSync(moduleDir, { recursive: true })
})

afterAll(() => {
  // Clean up — root is two levels up from projectDir
  const root = path.dirname(projectDir)
  rmSync(root, { recursive: true, force: true })
})

// ─── Tests ───────────────────────────────────────────────────────────────

describe('audit-dev-actions-alignment / endpoint drift detection', () => {
  it('detects pagespec ↔ service drift (ACTION-DRIFT-002, the canonical 405 bug)', async () => {
    writeFixtures(SERVICE_TS_DRIFT)
    const report = await audit(makeArgs())

    // We should find at least the drift for syncFromPce
    const drift002 = report.findings.filter(f => f.code === 'ACTION-DRIFT-002')
    expect(drift002.length).toBeGreaterThan(0)
    const syncDrift = drift002.find(f => f.detail?.serviceUrl?.includes('sync-from-pce'))
    expect(syncDrift).toBeDefined()
    expect(syncDrift?.severity).toBe('err')
    expect(syncDrift?.entity).toBe('TypeAffaire')
  })

  it('detects verb mismatch for analyze-impact (ACTION-DRIFT-005, GET vs POST)', async () => {
    writeFixtures(SERVICE_TS_DRIFT)
    const report = await audit(makeArgs())

    // The pagespec says GET /impact but the service does POST /analyze-impact —
    // depending on candidate matching it surfaces as DRIFT-002 (URL diff) or
    // DRIFT-005 (verb diff). Either is acceptable; the canonical fix
    // applies the same way.
    const relevant = report.findings.filter(
      f =>
        (f.detail?.serviceUrl?.includes('analyze-impact') || f.detail?.serviceUrl?.includes('impact')) &&
        f.entity === 'TypeAffaire',
    )
    expect(relevant.length).toBeGreaterThan(0)
    expect(relevant.every(f => f.severity === 'err')).toBe(true)
  })

  it('produces zero err findings when pagespec, controller, and service are aligned', async () => {
    writeFixtures(SERVICE_TS_ALIGNED)
    const report = await audit(makeArgs())

    expect(report.counts.err).toBe(0)
    // The OK finding is emitted when no drift is detected.
    const ok = report.findings.find(f => f.code === 'ACTION-DRIFT-OK')
    expect(ok).toBeDefined()
  })

  it('inventory counts reflect what was scanned', async () => {
    writeFixtures(SERVICE_TS_ALIGNED)
    const report = await audit(makeArgs())

    expect(report.inventory.pageActions).toBeGreaterThanOrEqual(2) // syncFromPce + analyzeImpact (open is navigate → excluded)
    expect(report.inventory.controllerEndpoints).toBeGreaterThanOrEqual(2)
    expect(report.inventory.serviceCalls).toBeGreaterThanOrEqual(2)
    expect(report.inventory.entitiesCovered).toContain('TypeAffaire')
  })

  it('emits a markdown report with verdict header', async () => {
    writeFixtures(SERVICE_TS_DRIFT)
    const report = await audit(makeArgs())

    expect(report.markdown).toContain('# Actions alignment audit')
    expect(report.markdown).toContain('Verdict :')
    // The drift report should highlight pagespec ↔ service issues
    expect(report.markdown).toContain('TypeAffaire')
  })

  it('excludes kind:navigate actions from the alignment check', async () => {
    writeFixtures(SERVICE_TS_ALIGNED)
    const report = await audit(makeArgs())

    // The `open` action is kind:navigate — it must NOT be flagged as missing
    // even though no endpoint backs it (intentional: navigations bypass the API).
    const navigateDrifts = report.findings.filter(
      f => f.detail?.pageActionEndpoint === 'open' || f.message.toLowerCase().includes('"open"'),
    )
    expect(navigateDrifts).toEqual([])
  })
})
