import { describe, it, expect } from 'vitest'
import {
  computeImportFanInFromGraph,
  computeTestMappingsFromGraph,
  computeCombinedHotspots,
} from '../hotspots.js'
import type { ArchitectureGraph } from '../types.js'

function makeGraph(deps: Record<string, string[]>): ArchitectureGraph {
  return {
    version: 1,
    dependencies: deps,
    entryPoints: [],
    modules: {},
    builtAt: 0,
    rootDir: '/',
  }
}

describe('computeImportFanInFromGraph', () => {
  it('returns empty list for null graph', () => {
    expect(computeImportFanInFromGraph(null)).toEqual([])
  })

  it('counts unique importers per file', () => {
    const g = makeGraph({
      'a.ts': ['shared.ts'],
      'b.ts': ['shared.ts'],
      'c.ts': ['shared.ts', 'other.ts'],
    })
    const fanIn = computeImportFanInFromGraph(g)
    expect(fanIn[0]).toEqual({ path: 'shared.ts', importers: 3 })
    expect(fanIn[1]).toEqual({ path: 'other.ts', importers: 1 })
  })

  it('orders by descending importer count', () => {
    const g = makeGraph({
      'a.ts': ['x.ts'],
      'b.ts': ['y.ts', 'x.ts'],
      'c.ts': ['y.ts', 'x.ts'],
    })
    const fanIn = computeImportFanInFromGraph(g)
    expect(fanIn[0].path).toBe('x.ts')
    expect(fanIn[1].path).toBe('y.ts')
  })
})

describe('computeTestMappingsFromGraph', () => {
  it('returns empty for null graph', () => {
    expect(computeTestMappingsFromGraph(null)).toEqual([])
  })

  it('pairs source files with .test.ts siblings', () => {
    const g = makeGraph({
      'src/util.ts': [],
      'src/util.test.ts': ['src/util.ts'],
      'src/other.ts': [],
    })
    const mappings = computeTestMappingsFromGraph(g)
    expect(mappings).toContainEqual({
      source: 'src/util.ts',
      test: 'src/util.test.ts',
    })
    expect(mappings.some((m) => m.source === 'src/other.ts')).toBe(false)
  })

  it('recognizes __tests__ subdir tests', () => {
    const g = makeGraph({
      'src/util.ts': [],
      'src/__tests__/util.ts': ['src/util.ts'],
    })
    const mappings = computeTestMappingsFromGraph(g)
    expect(mappings).toContainEqual({
      source: 'src/util.ts',
      test: 'src/__tests__/util.ts',
    })
  })
})

describe('computeCombinedHotspots (churn × fan-in × coverage)', () => {
  const graph = makeGraph({
    'core.ts': [],
    'util.ts': [],
    'util.test.ts': ['util.ts'],
    'a.ts': ['core.ts', 'util.ts'],
    'b.ts': ['core.ts'],
  })
  const changeFrequency = [
    { path: 'core.ts', changes: 20 },
    { path: 'util.ts', changes: 5 },
    { path: 'a.ts', changes: 2 },
  ]

  it('ranks high-churn high-fan-in untested file first', () => {
    const out = computeCombinedHotspots(graph, changeFrequency)
    expect(out[0].path).toBe('core.ts')
  })

  it('marks files without tests as untested', () => {
    const out = computeCombinedHotspots(graph, changeFrequency)
    const core = out.find((h) => h.path === 'core.ts')
    const util = out.find((h) => h.path === 'util.ts')
    expect(core?.hasTest).toBe(false)
    expect(util?.hasTest).toBe(true)
  })

  it('includes churn and fan-in in each entry', () => {
    const out = computeCombinedHotspots(graph, changeFrequency)
    const core = out.find((h) => h.path === 'core.ts')
    expect(core?.changes).toBe(20)
    expect(core?.importers).toBe(2)
  })

  it('gives untested files a larger score than tested files with equal churn+fan-in', () => {
    const g = makeGraph({
      'tested.ts': [],
      'tested.test.ts': ['tested.ts'],
      'untested.ts': [],
      'a.ts': ['tested.ts', 'untested.ts'],
    })
    const freq = [
      { path: 'tested.ts', changes: 10 },
      { path: 'untested.ts', changes: 10 },
    ]
    const out = computeCombinedHotspots(g, freq)
    const tested = out.find((h) => h.path === 'tested.ts')!
    const untested = out.find((h) => h.path === 'untested.ts')!
    expect(untested.score).toBeGreaterThan(tested.score)
  })

  it('returns empty list when no architecture is available', () => {
    expect(computeCombinedHotspots(null, changeFrequency)).toEqual([])
  })

  it('returns empty list when change frequency is empty', () => {
    expect(computeCombinedHotspots(graph, [])).toEqual([])
  })
})
