import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
  compileScaffoldRegistry,
  findEmptySourcePatterns,
  findMatchingEntries,
  findMissingDependentPaths,
  loadScaffoldRegistry,
  loadScaffoldRegistryFast,
  normalizeScaffoldPath,
  scaffoldPathMatchesPattern
} from '../index'
import type { ScaffoldRegistry, ScaffoldRegistryEntry } from '../schema'

function makeEntry(id: string, sources: string[]): ScaffoldRegistryEntry {
  return {
    id,
    kind: 'manual-scaffold',
    owner: 'packages/core',
    sources,
    dependents: [{ path: 'packages/core/package.json', regen: 'manual', hint: 'h' }]
  }
}

function registryFromEntries(entries: ScaffoldRegistryEntry[]): ScaffoldRegistry {
  return { version: '1', entries }
}

function matchedIds(registry: ScaffoldRegistry, filePath: string): string[] {
  return findMatchingEntries(registry, filePath).map((e) => e.id)
}

let tempRoots: string[] = []

function makeTempRoot(): string {
  const root = mkdtempSync(path.join(tmpdir(), 'scaffold-registry-'))
  tempRoots.push(root)
  return root
}

afterEach(() => {
  for (const root of tempRoots) {
    rmSync(root, { recursive: true, force: true })
  }
  tempRoots = []
})

describe('loadScaffoldRegistryFast', () => {
  it('preserves external sync metadata from the compiled registry', () => {
    const registry = loadScaffoldRegistryFast()
    const entry = registry.entries.find((candidate) => candidate.id === 'external-sync-managed-claude')

    expect(entry).toBeDefined()
    expect(entry).toMatchObject({
      owner: 'template',
      category: 'replace',
      strategy: 'replace-all',
      delete_policy: 'manifest-only'
    })
  })
})

describe('findMissingDependentPaths', () => {
  const baseEntry = {
    id: 'fake-entry',
    kind: 'manual-scaffold' as const,
    owner: 'packages/core',
    sources: ['packages/core/src/**'],
    dependents: [{ path: 'packages/core/this-file-does-not-exist.ts', regen: 'manual', hint: 'h' }]
  }

  it('flags dependents whose paths are missing on disk', () => {
    const registry: ScaffoldRegistry = { version: '1', entries: [baseEntry] }
    const missing = findMissingDependentPaths(registry, process.cwd())
    expect(missing).toEqual([{ entryId: 'fake-entry', path: 'packages/core/this-file-does-not-exist.ts' }])
  })

  it('skips symbolic targets, globs, and (self)', () => {
    const registry: ScaffoldRegistry = {
      version: '1',
      entries: [
        {
          ...baseEntry,
          dependents: [
            { path: 'docs: sync-preservation-matrix', regen: 'manual', hint: 'h' },
            { path: 'autogen-target:foo', regen: 'manual', hint: 'h' },
            { path: 'packages/core/src/**/*.ts', regen: 'manual', hint: 'h' },
            { path: '(self)', regen: 'manual', hint: 'h' }
          ]
        }
      ]
    }
    expect(findMissingDependentPaths(registry, process.cwd())).toEqual([])
  })

  it('skips sync-preservation entries (their paths target derived external projects)', () => {
    const registry: ScaffoldRegistry = {
      version: '1',
      entries: [
        {
          id: 'sync-entry',
          kind: 'sync-preservation',
          owner: '@elevasis/core',
          category: 'merge',
          strategy: 'merge-regions',
          delete_policy: 'none',
          sources: ['external/_template/foo.ts'],
          dependents: [{ path: 'core/src/foo-not-in-monorepo.ts', regen: 'manual', hint: 'h' }]
        }
      ]
    }
    expect(findMissingDependentPaths(registry, process.cwd())).toEqual([])
  })
})

describe('findMatchingEntries (pattern matching)', () => {
  it('normalizes Windows and relative paths', () => {
    expect(normalizeScaffoldPath('.\\packages\\core\\src\\')).toBe('packages/core/src')
  })

  it('matches exact paths', () => {
    const reg = registryFromEntries([makeEntry('exact', ['packages/core/src/index.ts'])])
    expect(matchedIds(reg, 'packages/core/src/index.ts')).toEqual(['exact'])
    expect(matchedIds(reg, 'packages/core/src/other.ts')).toEqual([])
  })

  it('matches /** suffix as deep prefix', () => {
    const reg = registryFromEntries([makeEntry('deep', ['packages/core/src/**'])])
    expect(matchedIds(reg, 'packages/core/src/foo.ts')).toEqual(['deep'])
    expect(matchedIds(reg, 'packages/core/src/nested/deeply/foo.ts')).toEqual(['deep'])
    expect(matchedIds(reg, 'packages/core/package.json')).toEqual([])
    expect(matchedIds(reg, 'packages/core/src')).toEqual([])
  })

  it('matches /* suffix as single-level prefix', () => {
    const reg = registryFromEntries([makeEntry('shallow', ['packages/*/package.json'])])
    expect(matchedIds(reg, 'packages/core/package.json')).toEqual(['shallow'])
    expect(matchedIds(reg, 'packages/ui/package.json')).toEqual(['shallow'])
    expect(matchedIds(reg, 'packages/core/nested/package.json')).toEqual([])
  })

  it('matches mixed **/*.ext patterns at direct-child and nested depths', () => {
    const reg = registryFromEntries([makeEntry('ext', ['apps/api/src/**/*.ts'])])
    expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual(['ext'])
    expect(matchedIds(reg, 'apps/api/src/nested/deeply/foo.ts')).toEqual(['ext'])
    expect(matchedIds(reg, 'apps/api/src/foo.tsx')).toEqual([])
  })

  it('normalizes Windows backslashes to forward slashes', () => {
    const reg = registryFromEntries([makeEntry('winpath', ['packages/core/src/**'])])
    expect(matchedIds(reg, 'packages\\core\\src\\foo.ts')).toEqual(['winpath'])
  })

  it('matches a directory-prefix pattern (no glob characters)', () => {
    const reg = registryFromEntries([makeEntry('dir', ['packages/core/src'])])
    expect(matchedIds(reg, 'packages/core/src/foo.ts')).toEqual(['dir'])
    expect(matchedIds(reg, 'packages/core/srcfoo.ts')).toEqual([])
  })

  it('returns multiple entries when several match the same file', () => {
    const reg = registryFromEntries([makeEntry('a', ['packages/core/src/**']), makeEntry('b', ['packages/core/src'])])
    expect(matchedIds(reg, 'packages/core/src/foo.ts').sort()).toEqual(['a', 'b'])
  })

  it('returns empty when no entries match', () => {
    const reg = registryFromEntries([makeEntry('a', ['packages/core/src/**'])])
    expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual([])
  })

  it('exposes the same matcher for direct consumers', () => {
    expect(scaffoldPathMatchesPattern('packages/core/src/foo.test.ts', 'packages/core/src/**/*.ts')).toBe(true)
    expect(scaffoldPathMatchesPattern('packages/core/src/foo.test.ts', 'packages/ui/src/**/*.ts')).toBe(false)
  })
})

describe('findEmptySourcePatterns', () => {
  it('flags source patterns that do not match any file or directory', () => {
    const root = makeTempRoot()
    mkdirSync(path.join(root, 'packages', 'core', 'src'), { recursive: true })
    writeFileSync(path.join(root, 'packages', 'core', 'src', 'index.ts'), 'export {}\n', 'utf8')

    const registry = registryFromEntries([
      makeEntry('ok-exact', ['packages/core/src/index.ts']),
      makeEntry('ok-glob', ['packages/core/src/**/*.ts']),
      makeEntry('empty', ['packages/core/src/**/*.tsx'])
    ])

    expect(findEmptySourcePatterns(registry, root)).toEqual([
      { entryId: 'empty', pattern: 'packages/core/src/**/*.tsx' }
    ])
  })

  it('treats exact directories as non-empty source coverage', () => {
    const root = makeTempRoot()
    mkdirSync(path.join(root, 'external', '_template', '.claude', 'skills'), { recursive: true })

    const registry = registryFromEntries([
      makeEntry('dir', ['external/_template/.claude/skills']),
      makeEntry('missing-dir', ['external/_template/.claude/commands'])
    ])

    expect(findEmptySourcePatterns(registry, root)).toEqual([
      { entryId: 'missing-dir', pattern: 'external/_template/.claude/commands' }
    ])
  })
})

// ---------------------------------------------------------------------------
// Fixture helpers for compileScaffoldRegistry / loadScaffoldRegistry tests
// ---------------------------------------------------------------------------

/**
 * Write a minimal scaffold-registry.yml into the fixture root.
 * `kind: manual-scaffold` is used to avoid the `regen` requirement of `kind: autogen`.
 */
function writeMinimalYaml(root: string, extraEntries?: string): void {
  mkdirSync(path.join(root, '.claude', 'registries'), { recursive: true })
  // Must have at least one real source path that exists so compileScaffoldRegistry
  // does not reject it with "source pattern matches no files". We create the file.
  mkdirSync(path.join(root, 'src'), { recursive: true })
  writeFileSync(path.join(root, 'src', 'index.ts'), 'export {}\n', 'utf8')
  const yaml = [
    'version: "1"',
    'entries:',
    '  - id: fixture-entry',
    '    kind: manual-scaffold',
    '    owner: packages/core',
    '    sources:',
    '      - src/index.ts',
    '    dependents:',
    '      - path: src/index.ts',
    ...(extraEntries ? [extraEntries] : [])
  ].join('\n')
  writeFileSync(path.join(root, '.claude', 'registries', 'scaffold-registry.yml'), yaml + '\n', 'utf8')
}

function compiledJsonPath(root: string): string {
  return path.join(root, '.claude', 'registries', 'scaffold-registry.compiled.json')
}

// ---------------------------------------------------------------------------
// compileScaffoldRegistry
// ---------------------------------------------------------------------------

describe('compileScaffoldRegistry', () => {
  it('writes scaffold-registry.compiled.json that round-trips the YAML entries', () => {
    const root = makeTempRoot()
    writeMinimalYaml(root)

    const registry = compileScaffoldRegistry(root)

    // Return value has the entry
    expect(registry.entries).toHaveLength(1)
    expect(registry.entries[0].id).toBe('fixture-entry')

    // Written file exists and is valid JSON
    const raw = readFileSync(compiledJsonPath(root), 'utf-8')
    const parsed = JSON.parse(raw) as { version: string; entries: Array<{ id: string }> }
    expect(parsed.version).toBe('1')
    expect(parsed.entries).toHaveLength(1)
    expect(parsed.entries[0].id).toBe('fixture-entry')
  })

  it('written JSON ends with a trailing newline', () => {
    const root = makeTempRoot()
    writeMinimalYaml(root)

    compileScaffoldRegistry(root)

    const raw = readFileSync(compiledJsonPath(root), 'utf-8')
    expect(raw.endsWith('\n')).toBe(true)
  })

  it('throws when a non-glob dependent path does not exist on disk', () => {
    const root = makeTempRoot()
    mkdirSync(path.join(root, '.claude', 'registries'), { recursive: true })
    mkdirSync(path.join(root, 'src'), { recursive: true })
    writeFileSync(path.join(root, 'src', 'index.ts'), 'export {}\n', 'utf8')
    // Dependent references a file that does NOT exist
    const yaml = [
      'version: "1"',
      'entries:',
      '  - id: missing-dep',
      '    kind: manual-scaffold',
      '    owner: packages/core',
      '    sources:',
      '      - src/index.ts',
      '    dependents:',
      '      - path: src/does-not-exist.ts'
    ].join('\n')
    writeFileSync(path.join(root, '.claude', 'registries', 'scaffold-registry.yml'), yaml + '\n', 'utf8')

    expect(() => compileScaffoldRegistry(root)).toThrow('scaffold-registry:')
    expect(() => compileScaffoldRegistry(root)).toThrow('dependent path(s) do not exist on disk')
    expect(() => compileScaffoldRegistry(root)).toThrow('[missing-dep] src/does-not-exist.ts')
  })

  it('throws when a source pattern matches no files or directories', () => {
    const root = makeTempRoot()
    mkdirSync(path.join(root, '.claude', 'registries'), { recursive: true })
    mkdirSync(path.join(root, 'src'), { recursive: true })
    writeFileSync(path.join(root, 'src', 'index.ts'), 'export {}\n', 'utf8')
    // Source glob matches nothing
    const yaml = [
      'version: "1"',
      'entries:',
      '  - id: empty-source',
      '    kind: manual-scaffold',
      '    owner: packages/core',
      '    sources:',
      '      - src/**/*.tsx',
      '    dependents:',
      '      - path: src/index.ts'
    ].join('\n')
    writeFileSync(path.join(root, '.claude', 'registries', 'scaffold-registry.yml'), yaml + '\n', 'utf8')

    expect(() => compileScaffoldRegistry(root)).toThrow('scaffold-registry:')
    expect(() => compileScaffoldRegistry(root)).toThrow('source pattern(s) match no files or directories')
    expect(() => compileScaffoldRegistry(root)).toThrow('[empty-source] src/**/*.tsx')
  })
})

// ---------------------------------------------------------------------------
// loadScaffoldRegistry — hash-drift detection
// ---------------------------------------------------------------------------

describe('loadScaffoldRegistry', () => {
  it('returns the registry when YAML and compiled JSON hashes match', () => {
    const root = makeTempRoot()
    writeMinimalYaml(root)
    // Compile first so the hashes are in sync
    compileScaffoldRegistry(root)

    const registry = loadScaffoldRegistry(root)
    expect(registry.entries[0].id).toBe('fixture-entry')
  })

  it('throws when compiled JSON is out of sync with YAML', () => {
    const root = makeTempRoot()
    writeMinimalYaml(root)
    // Compile to create a valid compiled JSON
    compileScaffoldRegistry(root)

    // Now write a DIFFERENT YAML (adds a new entry) without recompiling
    mkdirSync(path.join(root, 'src'), { recursive: true })
    writeFileSync(path.join(root, 'src', 'other.ts'), 'export {}\n', 'utf8')
    const updatedYaml = [
      'version: "1"',
      'entries:',
      '  - id: fixture-entry',
      '    kind: manual-scaffold',
      '    owner: packages/core',
      '    sources:',
      '      - src/index.ts',
      '    dependents:',
      '      - path: src/index.ts',
      '  - id: new-entry',
      '    kind: manual-scaffold',
      '    owner: packages/core',
      '    sources:',
      '      - src/other.ts',
      '    dependents:',
      '      - path: src/other.ts'
    ].join('\n')
    writeFileSync(path.join(root, '.claude', 'registries', 'scaffold-registry.yml'), updatedYaml + '\n', 'utf8')

    expect(() => loadScaffoldRegistry(root)).toThrow('compiled JSON is out of sync with YAML')
  })

  it('skips drift check silently when compiled JSON does not exist (ENOENT)', () => {
    const root = makeTempRoot()
    writeMinimalYaml(root)
    // No compiled JSON — ENOENT path should be silent (no throw)

    const registry = loadScaffoldRegistry(root)
    expect(registry.entries[0].id).toBe('fixture-entry')
  })
})
