// tests/generate-react-bindings.test.ts
import { describe, it, expect } from 'vitest'
import {
  renderWrapper,
  renderTypesInterface,
  renderRegisterFile,
  renderTypesFile,
} from '../scripts/lib/react-binding-templates.mjs'
import type { ComponentConfig } from '../src/web-components/components.config.ts'

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

const buttonCfg: ComponentConfig = {
  name: 'Button',
  tag: 'tvu-button',
  canonicalImport: 'ButtonBridge',
  canonicalPath: '../canonical/ButtonBridge.vue',
  figmaCodeComponent: 'Button',
  props: [
    { name: 'color',      tsType: `'gray 1' | 'green'` },
    { name: 'fill',       tsType: `'filling' | 'ghost' | 'rimless'`,
      jsdoc: 'Appearance/fill variant enum (filling/ghost/rimless).' },
    { name: 'theme',      tsType: `'dark' | 'light'` },
  ],
  events: [
    { reactHandlerProp: 'onClick', domEvent: 'click', handlerType: '(e: Event) => void' },
  ],
  vModel: null,
  hasDefaultSlot: true,
  namedSlots: [],
}

const inputCfg: ComponentConfig = {
  name: 'Input',
  tag: 'tvu-input',
  canonicalImport: 'InputBoxLine',
  canonicalPath: '../canonical/InputBoxLine.vue',
  figmaCodeComponent: 'InputBoxLine',
  props: [
    { name: 'placeholder', tsType: 'string' },
    { name: 'readonly',    tsType: 'boolean' },
  ],
  events: [],
  vModel: {
    reactValueProp:  'value',
    reactChangeProp: 'onChange',
    elementProperty: 'modelValue',
    updateEvent:     'update:modelValue',
    detailIndex:     0,
    valueType:       'string',
  },
  hasDefaultSlot: false,
  namedSlots: [],
}

const formItemCfg: ComponentConfig = {
  name: 'FormItem',
  tag: 'tvu-form-item',
  canonicalImport: 'FormItem',
  canonicalPath: '../canonical/FormItem.vue',
  figmaCodeComponent: 'FormItem',
  props: [
    { name: 'label',  tsType: 'string' },
    { name: 'layout', tsType: `'1 line' | '1 line & Right' | '2 lines'` },
  ],
  events: [],
  vModel: null,
  hasDefaultSlot: true,
  namedSlots: ['label'],
}

// Union-typed v-model (e.g. Switch's `status: 'off' | 'on' | 'live'`). '' is NOT
// a valid member, so the wrapper must NOT inject a `= ''` destructuring default.
const switchCfg: ComponentConfig = {
  name: 'Switch',
  tag: 'tvu-switch',
  canonicalImport: 'Switch',
  canonicalPath: '../canonical/Switch.vue',
  figmaCodeComponent: 'Switch',
  props: [
    { name: 'darkTheme', tsType: `'on' | 'off'` },
  ],
  events: [],
  vModel: {
    reactValueProp:  'status',
    reactChangeProp: 'onStatusChange',
    elementProperty: 'status',
    updateEvent:     'update:status',
    detailIndex:     0,
    valueType:       `'off' | 'on' | 'live'`,
  },
  hasDefaultSlot: false,
  namedSlots: [],
}

// Named-slot-only component (no default slot) — e.g. TopBar. Accepts children
// via light-DOM projection (`<TopBar><div slot="logo">…</div></TopBar>`).
const topBarCfg: ComponentConfig = {
  name: 'TopBar',
  tag: 'tvu-top-bar',
  canonicalImport: 'TopBar',
  canonicalPath: '../canonical/TopBar.vue',
  figmaCodeComponent: 'TopBar',
  props: [
    { name: 'title', tsType: 'string' },
  ],
  events: [],
  vModel: null,
  hasDefaultSlot: false,
  namedSlots: ['logo', 'left', 'search', 'menu', 'right-content'],
}

// Pagination-style component: a v-model PLUS a second Vue custom emit
// (`update:pageSize`) modeled as an events[] entry WITH detailIndex (the value
// rides in CustomEvent.detail[0]). The wrapper must unwrap detail[0] for that
// handler, not pass the raw Event.
const paginationCfg: ComponentConfig = {
  name: 'Pagination',
  tag: 'tvu-pagination',
  canonicalImport: 'Pagination',
  canonicalPath: '../canonical/Pagination.vue',
  figmaCodeComponent: 'Pagination',
  props: [
    { name: 'total',    tsType: 'number' },
    { name: 'pageSize', tsType: 'number' },
  ],
  events: [
    { reactHandlerProp: 'onPageSizeChange', domEvent: 'update:pageSize', handlerType: '(value: number) => void', detailIndex: 0 },
  ],
  vModel: {
    reactValueProp:  'value',
    reactChangeProp: 'onChange',
    elementProperty: 'modelValue',
    updateEvent:     'update:modelValue',
    detailIndex:     0,
    valueType:       'number',
  },
  hasDefaultSlot: false,
  namedSlots: [],
}

// ---------------------------------------------------------------------------
// From task-3-brief.md (the mandatory 3 tests)
// ---------------------------------------------------------------------------

it('exposes `fill` directly on the React surface (no reserved-name remap needed)', () => {
  const tsx = renderWrapper(buttonCfg)
  expect(tsx).toContain('fill')                      // React-facing prop is fill
  expect(tsx).toContain('(el as any).fill = fill')   // element property set via fill
  expect(tsx).not.toMatch(/\bvariant\b/)             // no old variant remap
})

it('types interface exposes `fill`, not `style` or `variant`', () => {
  const i = renderTypesInterface(buttonCfg)
  expect(i).toContain('fill?: ButtonFill')
  expect(i).not.toContain('variant?:')
  expect(i).not.toContain('style?:')
})

it('v-model component emits controlled value/onChange + listens on updateEvent', () => {
  const tsx = renderWrapper(inputCfg)
  expect(tsx).toContain('.modelValue = value')
  expect(tsx).toContain("addEventListener('update:modelValue'")
  expect(tsx).toContain('detail?.[0]')
})

it('string v-model gets a `= \'\'` destructuring default + guarded down-push', () => {
  const tsx = renderWrapper(inputCfg)
  // String value type → '' is a valid initial value, default present.
  expect(tsx).toContain("value = '',")
  // Down-push is always guarded against undefined.
  expect(tsx).toContain('if (value !== undefined) (el as any).modelValue = value')
})

it('non-string (union) v-model gets NO `= \'\'` default + guarded down-push', () => {
  const tsx = renderWrapper(switchCfg)
  // Union value type → '' is NOT a valid member; no destructuring default.
  expect(tsx).not.toContain("status = ''")
  // The prop is destructured bare (undefined when omitted).
  expect(tsx).toMatch(/^\s*status,$/m)
  // Down-push guards against undefined so an omitted prop is not pushed
  // (lets the Vue SFC's withDefaults default apply on initial mount).
  expect(tsx).toContain('if (status !== undefined) (el as any).status = status')
})

it('v-model listener guards detail[0] against undefined — does NOT coerce to \'\' (type-safe for union value types)', () => {
  const tsx = renderWrapper(inputCfg)
  // Guarded form: read detail[0], fire onChange only when not undefined.
  expect(tsx).toContain('const newValue = (e as CustomEvent).detail?.[0]')
  expect(tsx).toContain('if (newValue !== undefined) onChange(newValue)')
  // Must NOT use the old `?? ''` coercion ('' is not a valid union member).
  expect(tsx).not.toContain("?? ''")
})

describe('events[] handler — Vue emit (detailIndex) vs native DOM event', () => {
  it('events[] entry WITH detailIndex unwraps CustomEvent.detail and calls handler with the value', () => {
    const tsx = renderWrapper(paginationCfg)
    // Unwrapping form: read detail[0], fire the handler with the value (not the Event).
    expect(tsx).toContain('const v = (e as CustomEvent).detail?.[0]')
    expect(tsx).toContain('if (v !== undefined) onPageSizeChange(v)')
    expect(tsx).toContain("el.addEventListener('update:pageSize', handler)")
    // Must NOT wire the handler directly (that would pass the raw Event).
    expect(tsx).not.toContain("el.addEventListener('update:pageSize', onPageSizeChange)")
  })

  it('events[] entry WITHOUT detailIndex wires the handler directly (native Event passes through)', () => {
    const tsx = renderWrapper(buttonCfg)
    // Native click: handler receives the Event object directly — no detail unwrap.
    expect(tsx).toContain("el.addEventListener('click', onClick)")
    expect(tsx).toContain("el.removeEventListener('click', onClick)")
    // No detail unwrapping for the native event handler.
    expect(tsx).not.toContain('onClick(v)')
  })
})

// ---------------------------------------------------------------------------
// Additional tests
// ---------------------------------------------------------------------------

describe('renderRegisterFile / renderTypesFile', () => {
  it('register file contains all 3 tags', () => {
    const allThree = [buttonCfg, inputCfg, formItemCfg]
    const reg = renderRegisterFile(allThree)
    expect(reg).toContain('tvu-button')
    expect(reg).toContain('tvu-input')
    expect(reg).toContain('tvu-form-item')
  })

  it('primitive prop renders inline — no type alias emitted', () => {
    const types = renderTypesFile([inputCfg])
    // `string` and `boolean` are primitive — no alias like `InputPlaceholder = ...`
    expect(types).not.toContain('InputPlaceholder')
    expect(types).not.toContain('InputReadonly')
    // But the interface should contain the props
    expect(types).toContain('placeholder?: string')
    expect(types).toContain('readonly?: boolean')
  })
})

describe('renderWrapper — non-vModel no-default-slot component self-closes', () => {
  it('self-closes when hasDefaultSlot is false', () => {
    const tsx = renderWrapper(inputCfg)
    // tvu-input has no children → self-close
    expect(tsx).toContain('<tvu-input ref={ref} />')
  })

  it('has open+close tags when hasDefaultSlot is true', () => {
    const tsx = renderWrapper(formItemCfg)
    expect(tsx).toContain('<tvu-form-item ref={ref}>')
    expect(tsx).toContain('</tvu-form-item>')
    expect(tsx).toContain('{children}')
  })

  it('renders {children} for a named-slot-only component (no default slot) — light-DOM projection', () => {
    const tsx = renderWrapper(topBarCfg)
    expect(tsx).toContain('<tvu-top-bar ref={ref}>')
    expect(tsx).toContain('</tvu-top-bar>')
    expect(tsx).toContain('{children}')
    expect(tsx).not.toContain('<tvu-top-bar ref={ref} />')
  })

  it('TopBarProps interface includes children for a named-slot-only component', () => {
    const i = renderTypesInterface(topBarCfg)
    expect(i).toContain('children?: React.ReactNode')
  })
})

describe('named-slot props (first-class slot rendering)', () => {
  it('emits a React prop for each non-colliding named slot, camelCasing kebab names', () => {
    const tsx = renderWrapper(topBarCfg)
    // right-content → rightContent
    expect(tsx).toMatch(/^\s*rightContent,$/m)
    expect(tsx).toMatch(/^\s*logo,$/m)
    expect(tsx).toMatch(/^\s*menu,$/m)
    expect(tsx).toMatch(/^\s*left,$/m)
    expect(tsx).toMatch(/^\s*search,$/m)
  })

  it('renders each slot prop into a light-DOM <div slot="…"> with display:contents, guarded by undefined', () => {
    const tsx = renderWrapper(topBarCfg)
    expect(tsx).toContain(`{menu !== undefined ? <div slot="menu" style={{ display: 'contents' }}>{menu}</div> : null}`)
    expect(tsx).toContain(`{rightContent !== undefined ? <div slot="right-content" style={{ display: 'contents' }}>{rightContent}</div> : null}`)
    // children is still rendered (backward-compatible manual projection)
    expect(tsx).toContain('{children}')
  })

  it('declares slot props as React.ReactNode in the types interface', () => {
    const i = renderTypesInterface(topBarCfg)
    expect(i).toContain('rightContent?: React.ReactNode')
    expect(i).toContain('menu?: React.ReactNode')
    expect(i).toContain('logo?: React.ReactNode')
  })

  it('does NOT emit a slot prop when its name collides with an existing prop (FormItem `label` slot vs `label` prop)', () => {
    const tsx = renderWrapper(formItemCfg)
    const iface = renderTypesInterface(formItemCfg)
    // `label` is the string prop; the `label` slot must NOT create a duplicate.
    expect(iface).toContain('label?: string')
    // exactly one `label?:` line in the interface (no `label?: React.ReactNode` duplicate)
    expect(iface.match(/^\s*label\?:/gm)?.length).toBe(1)
    // no slot wrapper for label
    expect(tsx).not.toContain('<div slot="label"')
    // `label` destructured exactly once
    expect(tsx.match(/^\s*label,$/gm)?.length).toBe(1)
  })
})
