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 { AuditDevApiArgs } from '../types.js'

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

/** Pagespec declaring 2 api actions + 1 navigate action. */
const PAGESPEC_ALIGNED = `<!-- 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" }
  ]
}
\`\`\`
`

/** Controller aligned with the pagespec — DEV-API-010 should pass. */
const CONTROLLER_ALIGNED = `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(); }
}
`

/** Controller MISSING the syncFromPce endpoint → DEV-API-010 should err. */
const CONTROLLER_MISSING_SYNC = `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(); }

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

/** Controller with a PHANTOM endpoint not in any pagespec → DEV-API-011 should warn. */
const CONTROLLER_WITH_PHANTOM = `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() { return NoContent(); }

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

    [HttpPost("legacy-recompute")]
    public async Task<ActionResult> LegacyRecompute() { return NoContent(); }
}
`

// ─── Scaffolding ────────────────────────────────────────────────────────

let backendDir: string
let moduleDir: string

function writeFixtures(controllerCs: string, pagespec: string = PAGESPEC_ALIGNED) {
  const controllersDir = path.join(backendDir, 'src/TestApp.Api/Controllers/Catalog')
  mkdirSync(controllersDir, { recursive: true })
  writeFileSync(path.join(controllersDir, 'TypesAffaireController.cs'), controllerCs)

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

function makeArgs(): AuditDevApiArgs {
  return {
    backendPath: backendDir,
    modulePath: moduleDir,
    moduleCode: 'CATALOG',
    appCode: 'APP',
    mode: 'audit',
    rules: [],
  }
}

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

afterAll(() => {
  const root = path.dirname(backendDir)
  rmSync(root, { recursive: true, force: true })
})

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

describe('audit-dev-api / Phase 2 gate', () => {
  it('passes (0 err) when pagespec actions all land on the controller', async () => {
    writeFixtures(CONTROLLER_ALIGNED)
    const report = await audit(makeArgs())

    expect(report.counts.errors).toBe(0)
    const dev010 = report.findings.find((f) => f.code === 'DEV-API-010')
    expect(dev010?.severity).toBe('ok')
    expect(dev010?.label).toBe('DEV_API_010_ok')
  })

  it('flags DEV-API-010 err when a pagespec action is missing on the controller (canonical drift)', async () => {
    writeFixtures(CONTROLLER_MISSING_SYNC)
    const report = await audit(makeArgs())

    expect(report.counts.errors).toBeGreaterThan(0)
    const dev010 = report.findings.find((f) => f.code === 'DEV-API-010')
    expect(dev010?.severity).toBe('err')
    expect(dev010?.label).toBe('DEV_API_010_err')
    // The missing endpoint must be cited in params.actions
    const actions = (dev010?.params.actions as string) ?? ''
    expect(actions).toContain('TypeAffaire')
    expect(actions).toContain('POST')
    expect(actions).toContain('sync-from-proconcept')
  })

  it('flags DEV-API-011 warn for phantom controller endpoints not backed by the pagespec', async () => {
    writeFixtures(CONTROLLER_WITH_PHANTOM)
    const report = await audit(makeArgs())

    const dev011 = report.findings.find((f) => f.code === 'DEV-API-011')
    expect(dev011?.severity).toBe('warn')
    expect(dev011?.label).toBe('DEV_API_011_warn')
    const endpoints = (dev011?.params.endpoints as string) ?? ''
    expect(endpoints).toContain('legacy-recompute')
  })

  it('excludes kind:navigate actions from DEV-API-010 (they do not need a controller)', async () => {
    // With the aligned controller, the only api actions are syncFromPce + analyzeImpact;
    // `open` is navigate → must NOT trigger DEV-API-010 err.
    writeFixtures(CONTROLLER_ALIGNED)
    const report = await audit(makeArgs())

    const dev010 = report.findings.find((f) => f.code === 'DEV-API-010')
    expect(dev010?.severity).toBe('ok')
    // The 'open' action of kind:navigate must not be in any missing list
    const allFindingsStr = JSON.stringify(report.findings)
    expect(allFindingsStr).not.toMatch(/"open"/)
  })

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

    expect(report.markdown).toContain('# audit-dev-api')
    expect(report.markdown).toContain('Verdict')
    expect(report.markdown).toContain('APP/CATALOG')
  })

  it('--rules filter limits the rules executed', async () => {
    writeFixtures(CONTROLLER_MISSING_SYNC)
    const args = { ...makeArgs(), rules: ['DEV-API-011'] }
    const report = await audit(args)

    // Only DEV-API-011 should appear; DEV-API-010 was filtered out
    const dev010 = report.findings.find((f) => f.code === 'DEV-API-010')
    const dev011 = report.findings.find((f) => f.code === 'DEV-API-011')
    expect(dev010).toBeUndefined()
    expect(dev011).toBeDefined()
  })
})

// ─── DEV-API-012 / 013 / 014 — screen-driven stratum (Wave D) ────────────

/** Aligned screen controller: every pagespec view has its matching endpoint. */
const SCREEN_CONTROLLER_ALIGNED = `using Microsoft.AspNetCore.Mvc;
namespace TestApp.Api.Controllers.Catalog.Screens;

[ApiController]
[ApiExplorerSettings(GroupName = "screens")]
[Route("api/screens/typeaffaires")]
public class TypeAffairesScreenController : ControllerBase
{
    private readonly ITypeAffaireService _service;
    public TypeAffairesScreenController(ITypeAffaireService service) => _service = service;

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

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

/** Screen controller MISSING the list endpoint → DEV-API-012 err. */
const SCREEN_CONTROLLER_MISSING_LIST = `using Microsoft.AspNetCore.Mvc;
namespace TestApp.Api.Controllers.Catalog.Screens;

[ApiController]
[ApiExplorerSettings(GroupName = "screens")]
[Route("api/screens/typeaffaires")]
public class TypeAffairesScreenController : ControllerBase
{
    private readonly ITypeAffaireService _service;
    public TypeAffairesScreenController(ITypeAffaireService service) => _service = service;

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

/** Screen controller carrying a leftover TODO[SCREEN-…] marker → DEV-API-013 err. */
const SCREEN_CONTROLLER_WITH_TODO = `using Microsoft.AspNetCore.Mvc;
namespace TestApp.Api.Controllers.Catalog.Screens;

[ApiController]
[ApiExplorerSettings(GroupName = "screens")]
[Route("api/screens/typeaffaires")]
public class TypeAffairesScreenController : ControllerBase
{
    private readonly ITypeAffaireService _service;
    public TypeAffairesScreenController(ITypeAffaireService service) => _service = service;

    [HttpGet("list")]
    // TODO[SCREEN-SCR-APP-CAT-TYPEAFFAIRE-001]: implement assertion
    public async Task<ActionResult> GetList() { return Ok(); }
}
`

/** Screen controller that injects a DIFFERENT service than the integration controller → DEV-API-014 err. */
const SCREEN_CONTROLLER_FORKED_SERVICE = `using Microsoft.AspNetCore.Mvc;
namespace TestApp.Api.Controllers.Catalog.Screens;

[ApiController]
[ApiExplorerSettings(GroupName = "screens")]
[Route("api/screens/typeaffaires")]
public class TypeAffairesScreenController : ControllerBase
{
    private readonly ITypeAffaireScreenService _service;
    public TypeAffairesScreenController(ITypeAffaireScreenService service) => _service = service;

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

function writeScreenController(content: string): void {
  const dir = path.join(backendDir, 'src/TestApp.Api/Controllers/Catalog/Screens')
  mkdirSync(dir, { recursive: true })
  writeFileSync(path.join(dir, 'TypeAffairesScreenController.cs'), content)
}

describe('audit-dev-api / Phase 2b gate — DEV-API-012 / 013 / 014', () => {
  it('DEV-API-012 emits ok when no screen controllers exist (transition mode)', async () => {
    writeFixtures(CONTROLLER_ALIGNED, PAGESPEC_ALIGNED)
    // No screen controller written — should not flood the report with err
    const report = await audit({ ...makeArgs(), rules: ['DEV-API-012'] })
    const dev012 = report.findings.find(f => f.code === 'DEV-API-012')
    expect(dev012?.severity).toBe('ok')
    expect((dev012?.params as { note?: string }).note).toMatch(/not yet executed/)
  })

  it('DEV-API-012 passes when every pagespec view is covered by the screen controller', async () => {
    writeFixtures(CONTROLLER_ALIGNED, PAGESPEC_ALIGNED)
    writeScreenController(SCREEN_CONTROLLER_ALIGNED)
    const report = await audit({ ...makeArgs(), rules: ['DEV-API-012'] })
    const dev012 = report.findings.find(f => f.code === 'DEV-API-012')
    expect(dev012?.severity).toBe('ok')
  })

  it('DEV-API-012 errs when a pagespec view is missing on the screen controller', async () => {
    writeFixtures(CONTROLLER_ALIGNED, PAGESPEC_ALIGNED)
    writeScreenController(SCREEN_CONTROLLER_MISSING_LIST)
    const report = await audit({ ...makeArgs(), rules: ['DEV-API-012'] })
    const dev012 = report.findings.find(f => f.code === 'DEV-API-012')
    expect(dev012?.severity).toBe('err')
    const screens = (dev012?.params as { screens?: string }).screens ?? ''
    expect(screens).toContain('SCR-APP-CAT-TYPEAFFAIRE-001')
    expect(screens).toContain('GET list')
  })

  it('DEV-API-013 passes when no TODO[SCREEN- markers remain', async () => {
    writeFixtures(CONTROLLER_ALIGNED, PAGESPEC_ALIGNED)
    writeScreenController(SCREEN_CONTROLLER_ALIGNED)
    const report = await audit({ ...makeArgs(), rules: ['DEV-API-013'] })
    const dev013 = report.findings.find(f => f.code === 'DEV-API-013')
    expect(dev013?.severity).toBe('ok')
  })

  it('DEV-API-013 errs when a TODO[SCREEN-...] marker survives', async () => {
    writeFixtures(CONTROLLER_ALIGNED, PAGESPEC_ALIGNED)
    writeScreenController(SCREEN_CONTROLLER_WITH_TODO)
    const report = await audit({ ...makeArgs(), rules: ['DEV-API-013'] })
    const dev013 = report.findings.find(f => f.code === 'DEV-API-013')
    expect(dev013?.severity).toBe('err')
    expect((dev013?.params as { count?: number }).count).toBe(1)
  })

  it('DEV-API-014 passes when integration + screen controllers inject the same service', async () => {
    writeFixtures(CONTROLLER_ALIGNED, PAGESPEC_ALIGNED)
    writeScreenController(SCREEN_CONTROLLER_ALIGNED) // ITypeAffaireService, same as integration
    const report = await audit({ ...makeArgs(), rules: ['DEV-API-014'] })
    const dev014 = report.findings.find(f => f.code === 'DEV-API-014')
    expect(dev014?.severity).toBe('ok')
  })

  it('DEV-API-014 errs when the screen controller forks to a different service interface', async () => {
    writeFixtures(CONTROLLER_ALIGNED, PAGESPEC_ALIGNED)
    writeScreenController(SCREEN_CONTROLLER_FORKED_SERVICE) // ITypeAffaireScreenService ≠ ITypeAffaireService
    const report = await audit({ ...makeArgs(), rules: ['DEV-API-014'] })
    const dev014 = report.findings.find(f => f.code === 'DEV-API-014')
    expect(dev014?.severity).toBe('err')
    const entities = (dev014?.params as { entities?: string }).entities ?? ''
    expect(entities).toContain('TypeAffaire')
    expect(entities).toContain('integration=ITypeAffaireService')
    expect(entities).toContain('screens=ITypeAffaireScreenService')
  })
})
