/**
 * compute-page-diff — unit + guard tests.
 *
 * Covers the F4.1 test matrix:
 *  - canonical-hash determinism is tested in lib/__tests__/canonical-hash.test.ts
 *  - tests 4-9 : computeDiff spec-diff scenarios + extractFencedJson
 *  - test 10  : toRegenerate empty when nothing changed (skip signal)
 *  - tests 11-14 : disk-drift guard (silent-skip class) — these pin the
 *                  orchestrator's Phase 3a union contract using INJECTED
 *                  exists/errCount deps, so the union logic is unit-testable
 *                  without a real web project.
 */
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { canonicalHash } from '../../../../lib/canonical-hash.js'
import { computeDiff } from '../compute-diff.js'
import { extractFencedJson, scanPagespecs, loadSnapshot, SNAPSHOT_FILENAME } from '../scan-pagespecs.js'
import { computeDiskDrift, selectPagesToRegenerate } from '../disk-drift.js'

const SPEC_A = { screenCode: 'SCR-A-001', entity: 'TypeAffaire', view: 'list', columns: [{ key: 'code' }] }
const SPEC_B = { screenCode: 'SCR-B-001', entity: 'TypeAffaire', view: 'form', fields: ['code', 'libelle'] }

describe('extractFencedJson (compute-page-diff copy)', () => {
  it('extracts the first fenced json block (test 9)', () => {
    expect(extractFencedJson('# T\n\n```json\n{"x":1}\n```\n')).toBe('{"x":1}')
  })
  it('returns null when the json tag is absent', () => {
    expect(extractFencedJson('```\n{"x":1}\n```')).toBeNull()
  })
})

describe('computeDiff', () => {
  let moduleRoot: string
  beforeEach(() => {
    moduleRoot = mkdtempSync(join(tmpdir(), 'compute-page-diff-'))
    mkdirSync(join(moduleRoot, 'pagespecs'), { recursive: true })
  })
  afterEach(() => rmSync(moduleRoot, { recursive: true, force: true }))

  function writePagespec(name: string, spec: object): void {
    writeFileSync(join(moduleRoot, 'pagespecs', name), '```json\n' + JSON.stringify(spec) + '\n```\n', 'utf8')
  }
  function writeSnapshot(pages: Record<string, string>): void {
    writeFileSync(join(moduleRoot, SNAPSHOT_FILENAME), JSON.stringify({ lastRun: '2026-05-28T00:00:00Z', pages }), 'utf8')
  }

  it('treats every page as added when no snapshot exists (test 4)', () => {
    writePagespec('TypeAffaire.list.md', SPEC_A)
    writePagespec('TypeAffaire.form.md', SPEC_B)
    const r = computeDiff({ moduleRoot })
    expect(r.snapshotFound).toBe(false)
    expect(r.diff.added).toEqual(['TypeAffaire.form', 'TypeAffaire.list'])
    expect(r.diff.modified).toEqual([])
    expect(r.diff.unchanged).toEqual([])
    expect(r.toRegenerate).toEqual(['TypeAffaire.form', 'TypeAffaire.list'])
  })

  it('treats every page as unchanged when the snapshot matches (test 5 + test 10)', () => {
    writePagespec('TypeAffaire.list.md', SPEC_A)
    writePagespec('TypeAffaire.form.md', SPEC_B)
    writeSnapshot({ 'TypeAffaire.list': canonicalHash(SPEC_A), 'TypeAffaire.form': canonicalHash(SPEC_B) })
    const r = computeDiff({ moduleRoot })
    expect(r.snapshotFound).toBe(true)
    expect(r.diff.unchanged).toEqual(['TypeAffaire.form', 'TypeAffaire.list'])
    expect(r.diff.added).toEqual([])
    // test 10: spec-side regenerate set is empty → the orchestrator's skip signal
    expect(r.toRegenerate).toEqual([])
  })

  it('detects a single modified page (test 6)', () => {
    writePagespec('TypeAffaire.list.md', SPEC_A)
    writeSnapshot({ 'TypeAffaire.list': 'sha256-STALE' })
    const r = computeDiff({ moduleRoot })
    expect(r.diff.modified).toHaveLength(1)
    expect(r.diff.modified[0]).toMatchObject({ key: 'TypeAffaire.list', previousHash: 'sha256-STALE' })
    expect(r.diff.modified[0].currentHash).toBe(canonicalHash(SPEC_A))
    expect(r.toRegenerate).toEqual(['TypeAffaire.list'])
  })

  it('detects a single added page alongside an unchanged one (test 7)', () => {
    writePagespec('TypeAffaire.list.md', SPEC_A)
    writePagespec('TypeAffaire.form.md', SPEC_B)
    writeSnapshot({ 'TypeAffaire.list': canonicalHash(SPEC_A) })
    const r = computeDiff({ moduleRoot })
    expect(r.diff.added).toEqual(['TypeAffaire.form'])
    expect(r.diff.unchanged).toEqual(['TypeAffaire.list'])
    expect(r.toRegenerate).toEqual(['TypeAffaire.form'])
  })

  it('detects a removed page (in snapshot, no longer on disk) (test 8)', () => {
    writePagespec('TypeAffaire.list.md', SPEC_A)
    writeSnapshot({ 'TypeAffaire.list': canonicalHash(SPEC_A), 'TypeAffaire.detail': 'sha256-OLD' })
    const r = computeDiff({ moduleRoot })
    expect(r.diff.removed).toEqual(['TypeAffaire.detail'])
    expect(r.diff.unchanged).toEqual(['TypeAffaire.list'])
    expect(r.toRegenerate).toEqual([]) // removed pages are not regenerated
  })

  it('handles a mixed add/modify/unchanged/remove diff', () => {
    writePagespec('TypeAffaire.list.md', SPEC_A) // unchanged
    writePagespec('TypeAffaire.form.md', SPEC_B) // modified
    writePagespec('TypeAffaire.detail.md', { v: 1 }) // added
    writeSnapshot({
      'TypeAffaire.list': canonicalHash(SPEC_A),
      'TypeAffaire.form': 'sha256-STALE',
      'TypeAffaire.kanban': 'sha256-GONE',
    })
    const r = computeDiff({ moduleRoot })
    expect(r.diff.added).toEqual(['TypeAffaire.detail'])
    expect(r.diff.modified.map((m) => m.key)).toEqual(['TypeAffaire.form'])
    expect(r.diff.unchanged).toEqual(['TypeAffaire.list'])
    expect(r.diff.removed).toEqual(['TypeAffaire.kanban'])
    expect(r.toRegenerate).toEqual(['TypeAffaire.detail', 'TypeAffaire.form'])
  })

  it('warns and skips a malformed pagespec without aborting the diff', () => {
    writePagespec('TypeAffaire.list.md', SPEC_A)
    writeFileSync(join(moduleRoot, 'pagespecs', 'Broken.list.md'), '# no fenced block', 'utf8')
    const r = computeDiff({ moduleRoot })
    expect(r.diff.added).toEqual(['TypeAffaire.list'])
    expect(r.warnings.some((w) => w.includes('Broken.list.md'))).toBe(true)
  })
})

describe('scanPagespecs / loadSnapshot', () => {
  it('returns empty (no warning) when pagespecs/ is absent', () => {
    const empty = mkdtempSync(join(tmpdir(), 'cpd-empty-'))
    try {
      expect(scanPagespecs(empty)).toEqual({ pages: [], warnings: [] })
      expect(loadSnapshot(empty)).toEqual({ snapshot: { pages: {} }, found: false })
    } finally {
      rmSync(empty, { recursive: true, force: true })
    }
  })
})

describe('disk-drift guard (orchestrator Phase 3a union contract)', () => {
  it('flags an unchanged page whose .tsx is missing (test 11)', () => {
    const drift = computeDiskDrift(['TypeAffaire.list'], {
      exists: () => false,
      errCount: () => 0,
    })
    expect(drift).toEqual(['TypeAffaire.list'])
  })

  it('flags an unchanged page whose .tsx has err violations (test 12)', () => {
    const drift = computeDiskDrift(['TypeAffaire.list'], {
      exists: () => true,
      errCount: (k) => (k === 'TypeAffaire.list' ? 3 : 0),
    })
    expect(drift).toEqual(['TypeAffaire.list'])
  })

  it('does NOT flag an unchanged page that is present and valid (test 13)', () => {
    const drift = computeDiskDrift(['TypeAffaire.list', 'TypeAffaire.form'], {
      exists: () => true,
      errCount: () => 0,
    })
    expect(drift).toEqual([])
  })

  it('skips Phase 3a when spec-diff AND disk-drift are both empty (test 14)', () => {
    // all unchanged + all valid on disk → nothing to regenerate → Phase 3a skipped
    const diskDrift = computeDiskDrift(['A.list', 'B.form'], { exists: () => true, errCount: () => 0 })
    expect(selectPagesToRegenerate([], diskDrift)).toEqual([])
  })

  it('unions spec-diff with disk-drift, de-duplicated and sorted', () => {
    expect(selectPagesToRegenerate(['B.form', 'A.list'], ['A.list', 'C.detail'])).toEqual([
      'A.list',
      'B.form',
      'C.detail',
    ])
  })
})
