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 { extractFencedJson, parsePagespecFile, parsePagespecsForSection } from '../parse-pagespec.js'

describe('extractFencedJson', () => {
  it('extracts the JSON block between ```json and ```', () => {
    const md = '# Title\n\n```json\n{"x":1}\n```\n\nfooter'
    expect(extractFencedJson(md)).toBe('{"x":1}')
  })

  it('returns null when no fenced ```json block is present', () => {
    expect(extractFencedJson('# Title\n\nplain markdown body')).toBeNull()
    expect(extractFencedJson('# Title\n\n```\n{"x":1}\n```')).toBeNull() // no `json` tag
  })

  it('handles multiline JSON with arrays and nested objects', () => {
    const md = '```json\n{\n  "a": [1, 2, 3],\n  "b": { "c": "d" }\n}\n```'
    const out = extractFencedJson(md)!
    expect(out).toMatch(/"a": \[1, 2, 3\]/)
    expect(JSON.parse(out).b.c).toBe('d')
  })

  it('only matches the FIRST fenced block when multiple are present', () => {
    const md = '```json\n{"first":1}\n```\n\n```json\n{"second":2}\n```'
    expect(extractFencedJson(md)).toBe('{"first":1}')
  })
})

const VALID_PAGESPEC = {
  screenCode: 'SCR-CRM-PIPELINE-OPPORTUNITES-001',
  appCode: 'crm',
  module: 'pipeline',
  section: 'opportunites',
  entity: 'Opportunity',
  view: 'list',
  permission: 'pipeline.opportunites.read',
  columns: [],
  actions: [],
}

describe('parsePagespecFile', () => {
  it('parses a valid pagespec into a PageSpec', () => {
    const md = '# Title\n\n```json\n' + JSON.stringify(VALID_PAGESPEC) + '\n```'
    const { ok, warning } = parsePagespecFile('any.md', md)
    expect(warning).toBeNull()
    expect(ok?.screenCode).toBe('SCR-CRM-PIPELINE-OPPORTUNITES-001')
    expect(ok?.view).toBe('list')
  })

  it('emits a warning when no fenced block is found', () => {
    const { ok, warning } = parsePagespecFile('any.md', '# Just markdown')
    expect(ok).toBeNull()
    expect(warning?.reason).toMatch(/no fenced/)
  })

  it('emits a warning when the JSON is malformed', () => {
    const { ok, warning } = parsePagespecFile('any.md', '```json\n{ not json }\n```')
    expect(ok).toBeNull()
    expect(warning?.reason).toMatch(/invalid JSON/)
  })

  it('emits a warning when the schema is violated (bad screenCode)', () => {
    const bad = { ...VALID_PAGESPEC, screenCode: 'NOT-A-SCR-CODE' }
    const md = '```json\n' + JSON.stringify(bad) + '\n```'
    const { ok, warning } = parsePagespecFile('any.md', md)
    expect(ok).toBeNull()
    expect(warning?.reason).toMatch(/schema violation/)
  })

  it('accepts unknown fields (passthrough — i18nKeys, refinement, etc.)', () => {
    const enriched = { ...VALID_PAGESPEC, i18nKeys: { fr: { foo: 'bar' } }, needsRefinement: false }
    const md = '```json\n' + JSON.stringify(enriched) + '\n```'
    const { ok, warning } = parsePagespecFile('any.md', md)
    expect(warning).toBeNull()
    expect(ok).not.toBeNull()
  })
})

describe('parsePagespecsForSection', () => {
  let baseDir: string
  beforeEach(() => {
    baseDir = mkdtempSync(join(tmpdir(), 'parse-pagespec-'))
    mkdirSync(join(baseDir, 'pagespecs'), { recursive: true })
  })
  afterEach(() => rmSync(baseDir, { recursive: true, force: true }))

  function write(name: string, spec: object): void {
    writeFileSync(join(baseDir, 'pagespecs', name), '```json\n' + JSON.stringify(spec) + '\n```', 'utf8')
  }

  it('returns only pagespecs whose section matches', () => {
    write('A.md', { ...VALID_PAGESPEC, section: 'opportunites' })
    write('B.md', { ...VALID_PAGESPEC, section: 'offers', screenCode: 'SCR-CRM-PIPELINE-OFFERS-001' })
    const r = parsePagespecsForSection(baseDir, 'opportunites')
    expect(r.pagespecs.length).toBe(1)
    expect(r.pagespecs[0].section).toBe('opportunites')
  })

  it('additionally filters by entity when provided', () => {
    write('A.md', { ...VALID_PAGESPEC, entity: 'Opportunity' })
    write('B.md', { ...VALID_PAGESPEC, entity: 'Pipeline', screenCode: 'SCR-CRM-PIPELINE-OPPORTUNITES-002' })
    const r = parsePagespecsForSection(baseDir, 'opportunites', 'Opportunity')
    expect(r.pagespecs.length).toBe(1)
    expect(r.pagespecs[0].entity).toBe('Opportunity')
  })

  it('sorts pagespecs by view order (list → detail → form → …)', () => {
    write('F.md', { ...VALID_PAGESPEC, view: 'form', screenCode: 'SCR-X-001' })
    write('L.md', { ...VALID_PAGESPEC, view: 'list', screenCode: 'SCR-X-002' })
    write('D.md', { ...VALID_PAGESPEC, view: 'detail', screenCode: 'SCR-X-003' })
    const r = parsePagespecsForSection(baseDir, 'opportunites')
    expect(r.pagespecs.map(p => p.view)).toEqual(['list', 'detail', 'form'])
  })

  it('returns empty result when the pagespecs/ directory is missing', () => {
    const empty = mkdtempSync(join(tmpdir(), 'parse-pagespec-empty-'))
    try {
      const r = parsePagespecsForSection(empty, 'opportunites')
      expect(r.pagespecs).toEqual([])
      expect(r.warnings).toEqual([])
    } finally {
      rmSync(empty, { recursive: true, force: true })
    }
  })

  it('skips non-md files and collects warnings for malformed ones', () => {
    write('A.md', VALID_PAGESPEC)
    writeFileSync(join(baseDir, 'pagespecs', 'B.md'), '# no fenced block here', 'utf8')
    writeFileSync(join(baseDir, 'pagespecs', 'notes.txt'), 'ignored', 'utf8')
    const r = parsePagespecsForSection(baseDir, 'opportunites')
    expect(r.pagespecs.length).toBe(1)
    expect(r.warnings.length).toBe(1)
    expect(r.warnings[0].file).toMatch(/B\.md$/)
  })
})
