/**
 * Unit tests for ThinkingBlock streaming redesign (U1).
 *
 * Covers:
 * - Streaming: expanded by default, streaming cursor class on text, accumulated chars visible.
 * - Auto-collapse to summary bar when isStreaming flips true -> false.
 * - Click summary bar to expand, full content revealed.
 * - Scroll position preserved across collapse/expand cycles.
 * - Empty content (no streaming) renders nothing.
 * - Streaming with empty content shows loading placeholder, then transitions.
 *
 * Mount strategy: native Vue createApp + reactive props wrapper (no @vue/test-utils
 * dependency — happy-dom + vi.mock stubs keep the suite hermetic).
 */

import { describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h, nextTick, reactive } from 'vue'

// Stub ant-design-vue Spin: avoids portal/resize side effects under happy-dom.
vi.mock('ant-design-vue', async () => {
  const { defineComponent, h } = await import('vue')
  return {
    Spin: defineComponent({
      name: 'ASpin',
      props: {
        size: { type: String, default: 'default' },
        spinning: { type: Boolean, default: true },
      },
      setup() {
        return () => h('span', { class: 'ant-spin-stub' })
      },
    }),
  }
})

// Stub icons to plain <i> elements; class bindings stay testable.
vi.mock('@ant-design/icons-vue', async () => {
  const { defineComponent, h } = await import('vue')
  return {
    BulbOutlined: defineComponent({
      name: 'BulbOutlined',
      setup() {
        return () => h('i', { class: 'icon-stub icon-bulb' })
      },
    }),
    RightOutlined: defineComponent({
      name: 'RightOutlined',
      setup() {
        return () => h('i', { class: 'icon-stub icon-right' })
      },
    }),
  }
})

const { default: ThinkingBlock } = await import('@/components/chat/ThinkingBlock.vue')

interface MountHandle {
  host: HTMLElement
  props: Record<string, unknown>
  unmount: () => void
}

function mountThinkingBlock(initialProps: Record<string, unknown>): MountHandle {
  const propsState = reactive<Record<string, unknown>>({ ...initialProps })
  const host = document.createElement('div')
  document.body.appendChild(host)
  const Wrapper = defineComponent({
    render() {
      return h(ThinkingBlock as never, { ...propsState } as never)
    },
  })
  const app = createApp(Wrapper)
  app.mount(host)
  return {
    host,
    props: propsState,
    unmount() {
      app.unmount()
      host.remove()
    },
  }
}

describe('ThinkingBlock', () => {
  it('streams expanded with cursor class and accumulated chars', async () => {
    const { host, props, unmount } = mountThinkingBlock({
      content: '正在思考',
      isStreaming: true,
    })
    await nextTick()

    const text = host.querySelector('.thinking-block__text') as HTMLElement
    expect(text).toBeTruthy()
    expect(text.textContent).toBe('正在思考')
    // Streaming cursor class applied (CSS ::after caret is driven by this class).
    expect(text.classList.contains('thinking-block__text--streaming')).toBe(true)
    // Spinner visible while streaming.
    expect(host.querySelector('.thinking-block__spinner')).toBeTruthy()

    // Accumulate more tokens — content prop is already the streamed aggregate.
    props.content = '正在思考：分析问题'
    await nextTick()
    expect((host.querySelector('.thinking-block__text') as HTMLElement).textContent).toBe(
      '正在思考：分析问题',
    )

    unmount()
  })

  it('auto-collapses to summary bar when streaming ends', async () => {
    const { host, props, unmount } = mountThinkingBlock({
      content: 'abcdefgh', // 8 chars
      isStreaming: true,
    })
    await nextTick()

    // Streaming: content visible.
    expect(host.querySelector('.thinking-block__text')).toBeTruthy()

    // Stream ends.
    props.isStreaming = false
    await nextTick()

    // Collapsed: content hidden.
    expect(host.querySelector('.thinking-block__text')).toBeNull()
    // Summary meta visible: HH:MM:SS · <chars> 字符.
    const meta = host.querySelector('.thinking-block__meta') as HTMLElement
    expect(meta).toBeTruthy()
    expect(meta.textContent).toMatch(/^\d{2}:\d{2}:\d{2} · 8 字符$/)

    unmount()
  })

  it('expands on click to reveal full content', async () => {
    const { host, unmount } = mountThinkingBlock({
      content: '完整的思考内容',
      isStreaming: false,
    })
    await nextTick()

    // Finished: collapsed by default (summary bar only).
    expect(host.querySelector('.thinking-block__text')).toBeNull()

    // Click summary bar to expand.
    ;(host.querySelector('.thinking-block__header') as HTMLElement).click()
    await nextTick()

    const text = host.querySelector('.thinking-block__text') as HTMLElement
    expect(text).toBeTruthy()
    expect(text.textContent).toBe('完整的思考内容')

    unmount()
  })

  it('preserves scroll position across collapse/expand cycles', async () => {
    const longText = '行\n'.repeat(400)
    const { host, unmount } = mountThinkingBlock({
      content: longText,
      isStreaming: false,
    })
    await nextTick()

    // Finished: collapsed first; expand manually.
    ;(host.querySelector('.thinking-block__header') as HTMLElement).click()
    await nextTick()

    let text = host.querySelector('.thinking-block__text') as HTMLElement
    text.scrollTop = 120

    // Collapse: component captures scrollTop before unmounting content.
    ;(host.querySelector('.thinking-block__header') as HTMLElement).click()
    await nextTick()
    expect(host.querySelector('.thinking-block__text')).toBeNull()

    // Expand again: scroll position restored on the freshly rendered node.
    ;(host.querySelector('.thinking-block__header') as HTMLElement).click()
    await nextTick()

    text = host.querySelector('.thinking-block__text') as HTMLElement
    expect(text.scrollTop).toBe(120)

    unmount()
  })

  it('renders nothing when content is empty and not streaming', () => {
    const { host, unmount } = mountThinkingBlock({ content: '' })
    expect(host.querySelector('.thinking-block')).toBeNull()
    unmount()
  })

  it('shows loading placeholder when streaming with empty content, then transitions', async () => {
    const { host, props, unmount } = mountThinkingBlock({
      content: '',
      isStreaming: true,
    })
    await nextTick()

    const root = host.querySelector('.thinking-block') as HTMLElement
    expect(root).toBeTruthy()
    expect(root.classList.contains('thinking-block--loading')).toBe(true)
    // Loading label.
    expect((host.querySelector('.thinking-block__label') as HTMLElement).textContent).toBe(
      '正在思考…',
    )
    // No content area while empty.
    expect(host.querySelector('.thinking-block__text')).toBeNull()

    // Once content arrives, exits loading and shows text.
    props.content = '开始思考'
    await nextTick()
    expect(root.classList.contains('thinking-block--loading')).toBe(false)
    expect(host.querySelector('.thinking-block__text')).toBeTruthy()

    unmount()
  })
})
