import { describe, it, expect, afterEach } from 'vitest'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import {
  parseDefinePropsBlock,
  extractProps,
  extractBarrelExports,
  collectCanonicalProps,
  scanCanonicalProps,
} from '../scripts/lib/canonical-props.mjs'

const REPO_ROOT = resolve(__dirname, '..')

describe('parseDefinePropsBlock', () => {
  it('withDefaults 包裹也能取到块', () => {
    const sfc = `<script setup lang="ts">
const props = withDefaults(defineProps<{
  fill?: string
  size?: number
}>(), { fill: 'solid' })
</script>`
    expect(parseDefinePropsBlock(sfc)).toContain('fill?: string')
  })

  it('裸 defineProps 也能取到块', () => {
    const sfc = `defineProps<{ name: string }>()`
    expect(parseDefinePropsBlock(sfc)).toContain('name: string')
  })

  it('无 defineProps → null（零 props 组件的形态）', () => {
    expect(parseDefinePropsBlock('<script setup lang="ts">\nconst x = 1\n</script>')).toBeNull()
  })

  it('单行内嵌套对象不提前截断（当前 39 个组件的真实形态）', () => {
    const sfc = `defineProps<{
  items?: Array<{ id: string; label: string }>
  fill?: string
}>()`
    const block = parseDefinePropsBlock(sfc)
    expect(block).toContain('items?')
    expect(block).toContain('fill?')
  })

  it('跨行嵌套对象也不截断（将来才会踩的脆性）', () => {
    const sfc = `defineProps<{
  config?: {
    nested: string
  }
  fill?: string
}>()`
    const block = parseDefinePropsBlock(sfc)
    expect(block).toContain('fill?')
  })
})

describe('extractProps', () => {
  it('跨行嵌套的内层字段不得被当成顶层 prop', () => {
    const sfc = `defineProps<{
  config?: {
    nested: string
  }
  fill?: string
}>()`
    const props = extractProps(sfc)
    expect([...props!.keys()].sort()).toEqual(['config', 'fill'])
    expect(props!.has('nested')).toBe(false)
  })

  it('跨行嵌套 prop 的值需完整（不能是单独的括号）', () => {
    const sfc = `defineProps<{
  config?: {
    nested: string
  }
  fill?: string
}>()`
    const props = extractProps(sfc)
    // 值应该包含嵌套的字段名，证明括号内容被完整累积
    expect(props!.get('config')).toContain('nested')
    expect(props!.get('config')).toContain('{')
    expect(props!.get('fill')).toBe('string')
  })

  it('保留 inline union 的完整类型表达式', () => {
    const props = extractProps(`defineProps<{ size?: number | string }>()`)
    expect(props!.get('size')).toBe('number | string')
  })

  it('单行内嵌套对象的值保持完整（回归护栏）', () => {
    const sfc = `defineProps<{
  items?: Array<{ id: string; label: string }>
  fill?: string
}>()`
    const props = extractProps(sfc)
    // 单行内嵌套仍应完整
    expect(props!.get('items')).toContain('Array')
    expect(props!.get('items')).toContain('id: string')
    expect(props!.get('fill')).toBe('string')
  })
})

// 三态（头注释 ③）：解析失败必须与「真的零 props」用不同的返回值表示，否则闸会把
// 读不出来的组件印成「活源实况：零 props（该组件只转发 attrs / 槽）」—— 一句它自己
// 编的活源断言。三条各锁一态。
// 可证伪性：把 (a)(b) 折叠回同一个返回值（无论折成 null 还是折成空 Map），(a) 或 (b)
// 里必有一条红——两条一起看才是这个区分的证明，单看 (b) 不足以（旧实现对别名形态
// 也返回 null，区别在于旧实现对 (a) 同样返回 null）。
describe('extractProps 三态：没有 props ≠ 解析失败', () => {
  it('(a) 文件里根本没有 defineProps → 空 Map（真的零 props）', () => {
    const sfc = `<script setup lang="ts">\nconst x = 1\n</script>\n<template><slot /></template>`
    const props = extractProps(sfc)
    expect(props).toBeInstanceOf(Map)
    expect(props!.size).toBe(0)
  })

  it('(b) 别名形态 defineProps<Props>() → null（解析失败，不是零 props）', () => {
    expect(extractProps(`<script setup lang="ts">\ndefineProps<Props>()\n</script>`)).toBeNull()
  })

  it('(b) withDefaults(defineProps<Props>(), …) 也是 null（解析失败）', () => {
    const sfc = `<script setup lang="ts">
const props = withDefaults(defineProps<Props>(), { fill: 'solid' })
</script>`
    expect(extractProps(sfc)).toBeNull()
  })

  it('(c) 字面 defineProps<{ … }> → 非空 Map', () => {
    const props = extractProps(`defineProps<{ rowKey: string }>()`)
    expect(props!.has('rowKey')).toBe(true)
  })
})

// collect 层是旧实现真正说谎的地方（`out.set(comp, props ?? new Map())` 把解析失败
// 折成空 Map）。这里用临时 repoRoot 造齐三态，⛔ 不碰真实 src/canonical。
// 可证伪性：把折叠改回来（解析失败 → 空 Map 进表）⇒ unparsable 变空、props 里多出
// AliasProps ⇒ 本条三个 expect 里至少两个红。
describe('scanCanonicalProps：解析失败的组件不进 props 表，单独列名单', () => {
  let root: string | null = null
  afterEach(() => {
    if (root) rmSync(root, { recursive: true, force: true })
    root = null
  })

  it('三态混在一个目录里 → 各归各位', () => {
    root = mkdtempSync(join(tmpdir(), 'f133-canonical-'))
    mkdirSync(join(root, 'src/canonical'), { recursive: true })
    writeFileSync(join(root, 'src/canonical/ZeroProps.vue'), '<template><slot /></template>')
    writeFileSync(
      join(root, 'src/canonical/AliasProps.vue'),
      `<script setup lang="ts">\nconst p = withDefaults(defineProps<Props>(), {})\n</script>`,
    )
    writeFileSync(
      join(root, 'src/canonical/Normal.vue'),
      `<script setup lang="ts">\ndefineProps<{ rowKey: string }>()\n</script>`,
    )

    const { props, unparsable } = scanCanonicalProps(root)
    expect(unparsable).toEqual(['AliasProps'])
    expect(props.has('AliasProps')).toBe(false) // ⛔ 不许以空 Map 混进来
    expect(props.get('ZeroProps')!.size).toBe(0) // 真零 props 仍是空 Map
    expect(props.get('Normal')!.has('rowKey')).toBe(true)
    // collectCanonicalProps 是 scanCanonicalProps 的 props 一半（既有调用点不变）
    expect([...collectCanonicalProps(root).keys()].sort()).toEqual(['Normal', 'ZeroProps'])
  })
})

describe('extractBarrelExports', () => {
  it('解析多行 export 块与 as 别名', () => {
    const ts = `export {
  Button,
  Badge as TvuBadge,
} from './canonical'
export type { TableColumn } from './components/Table/Table.vue'`
    const names = extractBarrelExports(ts)
    expect(names.has('Button')).toBe(true)
    expect(names.has('TvuBadge')).toBe(true)
    expect(names.has('Badge')).toBe(false) // 别名后的对外名才算
    expect(names.has('TableColumn')).toBe(true)
  })
})

describe('collectCanonicalProps（活源实测锚点）', () => {
  const live = collectCanonicalProps(REPO_ROOT)

  it('Breadcrumb 是零 props（空 Map，不是 null）', () => {
    expect(live.get('Breadcrumb')).toBeInstanceOf(Map)
    expect(live.get('Breadcrumb')!.size).toBe(0)
  })

  it('Table 含 rowKey / selectedKeys / loading', () => {
    const t = live.get('Table')!
    expect(t.has('rowKey')).toBe(true)
    expect(t.has('selectedKeys')).toBe(true)
    expect(t.has('loading')).toBe(true)
  })

  it('Tab 与 TabList 的 props 不相交于 items（二选一的机械证据）', () => {
    expect(live.get('Tab')!.has('items')).toBe(false)
    expect(live.get('TabList')!.has('items')).toBe(true)
  })
})
