// Regression coverage for the slot-composition contract of Tab + TabItem.
//
// `<Tab v-model><TabItem value="x">` — the pattern the docs advertise, and the one the
// Figma source models (Tab List is a container whose members are slotted) — shipped unable
// to show an active item. Two independent causes, one per framework path:
//
//   Vue SFC   canonical TabItem defaulted state/color/fill to 'Normal'/'White'/'Line' and
//             forwarded them unconditionally, so BaseTabItem's `props.state ? ... : injected`
//             never reached the injected branch. Fix: those props are default-less.
//   CE/React  a slotted <tvu-tab-item> is its own Vue app, so inject cannot cross. Fix:
//             BaseTab mirrors the state onto item hosts as attributes (`syncSlottedItemState`).
//
// It stayed invisible because Tab had ZERO render-verification entries — figma-to-code-mapping
// pointed `Tab` at "Tab/Item", so the coverage audit credited the ITEM's entries to the shell.
// These tests exist so the contract can never silently regress again.
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, h, nextTick } from 'vue'
import Tab from '../src/canonical/Tab.vue'
import TabItem from '../src/canonical/TabItem.vue'

const harness = (tabProps: Record<string, unknown>, itemProps: Record<string, unknown>[] = [
  { value: 'overview' }, { value: 'sources' }, { value: 'monitor' },
]) => defineComponent({
  render: () => h(Tab, tabProps, {
    default: () => itemProps.map((p) => h(TabItem, { key: String(p.value), ...p }, () => String(p.value))),
  }),
})

const itemClasses = (wrapper: ReturnType<typeof mount>) =>
  wrapper.findAll('.tab-item').map((el) => el.classes().join(' '))

describe('Tab slot composition — active state reaches slotted items', () => {
  it('activates the item whose value matches modelValue', () => {
    const wrapper = mount(harness({ modelValue: 'sources' }))
    const classes = itemClasses(wrapper)
    expect(classes[0]).toContain('tab-item--normal')
    expect(classes[1]).toContain('tab-item--active')
    expect(classes[2]).toContain('tab-item--normal')
  })

  it('inherits the shell fill and active colour (items pass neither)', () => {
    const wrapper = mount(harness({ modelValue: 'overview', fill: 'Filled', color: 'Green' }))
    const first = itemClasses(wrapper)[0]
    expect(first).toContain('tab-item--filled')
    expect(first).toContain('tab-item--active-green')
  })

  it('follows modelValue changes', async () => {
    const wrapper = mount(harness({ modelValue: 'overview' }))
    expect(itemClasses(wrapper)[0]).toContain('tab-item--active')
    await wrapper.setProps({})
    // Re-mount with the other value: the shell is the single source of active state.
    const moved = mount(harness({ modelValue: 'monitor' }))
    await nextTick()
    expect(itemClasses(moved)[0]).toContain('tab-item--normal')
    expect(itemClasses(moved)[2]).toContain('tab-item--active')
  })

  it('an explicitly pinned item prop still wins over the shell', () => {
    // This is how the Figma variant grid and the 12 TabItem verifier entries drive single
    // items; the slot-composition fix must not take that away.
    const wrapper = mount(harness({ modelValue: 'overview', fill: 'Line', color: 'White' }, [
      { value: 'overview', state: 'Normal' },
      { value: 'sources', state: 'Active', color: 'Green', fill: 'Filled' },
    ]))
    const classes = itemClasses(wrapper)
    expect(classes[0]).toContain('tab-item--normal') // pinned Normal beats the value match
    expect(classes[1]).toContain('tab-item--active')
    expect(classes[1]).toContain('tab-item--active-green')
    expect(classes[1]).toContain('tab-item--filled')
  })

  it('emits change + update:modelValue when a slotted item is clicked', async () => {
    const wrapper = mount(harness({ modelValue: 'overview' }))
    await wrapper.findAll('.tab-item')[1].trigger('click')
    const tab = wrapper.findComponent(Tab)
    expect(tab.emitted('update:modelValue')?.[0]).toEqual(['sources'])
    expect(tab.emitted('change')?.[0]).toEqual(['sources'])
  })
})

describe('Tab custom-element bridge — mirrors state onto slotted item hosts', () => {
  // Exercises syncSlottedItemState() against real <tvu-tab-item> elements (undefined here,
  // so they stay inert hosts) — the CE/React path, where inject cannot reach.
  // modelValue lives on the ROOT wrapper so setProps() can drive it (test-utils only allows
  // setProps on the mounted root).
  const mountWithHosts = (tabProps: Record<string, unknown>, hosts: { value: string }[]) =>
    mount(
      defineComponent({
        props: { modelValue: { type: String, default: undefined } },
        setup(p) {
          return () => h(Tab, { ...tabProps, modelValue: p.modelValue }, {
            default: () => hosts.map((hst) => h('tvu-tab-item', { key: hst.value, value: hst.value })),
          })
        },
      }),
      { props: { modelValue: tabProps.modelValue as string } },
    )

  it('writes state/fill/color onto each host', () => {
    const wrapper = mountWithHosts({ modelValue: 'sources', fill: 'Filled', color: 'Green' }, [
      { value: 'overview' }, { value: 'sources' },
    ])
    const hosts = wrapper.element.querySelectorAll('tvu-tab-item')
    expect(hosts[0].getAttribute('state')).toBe('Normal')
    expect(hosts[1].getAttribute('state')).toBe('Active')
    for (const hst of hosts) {
      expect(hst.getAttribute('fill')).toBe('Filled')
      expect(hst.getAttribute('color')).toBe('Green')
    }
  })

  it('never clobbers a consumer-pinned axis, even when pinned AFTER mount', async () => {
    // The generated React wrappers assign props in useEffect, i.e. after mount. A
    // decide-ownership-once-at-mount bridge would have overwritten them.
    const wrapper = mountWithHosts({ modelValue: 'overview' }, [{ value: 'overview' }, { value: 'sources' }])
    const hosts = wrapper.element.querySelectorAll('tvu-tab-item')
    expect(hosts[1].getAttribute('state')).toBe('Normal')

    // Simulate React: set the prop as a DOM property after mount, then force a re-sync.
    ;(hosts[1] as unknown as Record<string, unknown>).state = 'Active'
    await wrapper.setProps({ modelValue: 'monitor' }) // shell update → bridge re-syncs
    await nextTick()

    // Consumer's pin survives; the other host still tracks the shell.
    expect((hosts[1] as unknown as Record<string, unknown>).state).toBe('Active')
    expect(hosts[0].getAttribute('state')).toBe('Normal')
  })
})
