import { createApp, defineComponent, h, nextTick, ref } from 'vue'
import type { Component } from 'vue'
import { afterEach, describe, expect, it, vi } from 'vitest'
import AppHeader from '../src/components/AppHeader.vue'
import AssistantMessage from '../src/components/AssistantMessage.vue'
import MessageList from '../src/components/MessageList.vue'
import SuggestionChips from '../src/components/SuggestionChips.vue'
import type { AssistantTurn, ChatTurn } from '../src/lib/models'
import type { TrackEvent } from '../src/lib/types'

const unmountCallbacks: Array<() => void> = []

function track(rank: number): TrackEvent {
  return {
    type: 'track',
    rank,
    track: {
      id: `track-${rank}`,
      uri: `spotify:track:${rank}`,
      title: `Track ${rank}`,
      artists: ['Artist'],
      album_name: 'Album',
      album_art_url: null,
      external_url: null,
    },
    justification: 'It fits.',
  }
}

function doneTurn(): AssistantTurn {
  return {
    id: 'assistant-1',
    role: 'assistant',
    query: 'Focused listening',
    status: 'done',
    requestId: 'request-1',
    intentSummary: 'Calm music for focused work.',
    candidateCount: 20,
    tracks: [track(1), track(2)],
    warnings: [{ type: 'warning', code: 'limited_pool', message: 'The pool was limited.' }],
    error: null,
    transportFailure: null,
    completion: { type: 'done', track_count: 2, total_ms: 12 },
    playlist: { status: 'idle', name: null, url: null, message: null },
  }
}

function mountComponent(component: Component, props: Record<string, unknown>): HTMLElement {
  const root = document.createElement('div')
  document.body.append(root)
  const app = createApp(
    defineComponent({
      render: () => h(component, props),
    }),
  )
  app.mount(root)
  unmountCallbacks.push(() => {
    app.unmount()
    root.remove()
  })
  return root
}

afterEach(() => {
  while (unmountCallbacks.length) unmountCallbacks.pop()?.()
  vi.restoreAllMocks()
})

describe('result presentation', () => {
  it('uses the app title as a fresh-chat control', () => {
    const onReset = vi.fn()
    const root = mountComponent(AppHeader, {
      auth: { status: 'anonymous', user: null, message: null },
      mode: 'demo',
      healthFailed: false,
      onReset,
    })
    const button = root.querySelector<HTMLButtonElement>(
      'button[aria-label="discovery-by-llm: start a new chat"]',
    )

    expect(button?.type).toBe('button')
    expect(button?.textContent).toContain('discovery-by-llm')
    button?.click()
    expect(onReset).toHaveBeenCalledOnce()
  })

  it('keeps completion context outside the focusable track region', () => {
    const root = mountComponent(AssistantMessage, { turn: doneTurn(), canSave: true })
    const region = root.querySelector<HTMLElement>(
      '[role="region"][aria-label="2 recommended tracks"]',
    )
    const summary = root.querySelector<HTMLElement>('.intent')
    const warning = root.querySelector<HTMLElement>('[data-warning-code="limited_pool"]')
    const action = root.querySelector<HTMLButtonElement>('button')

    expect(region?.tabIndex).toBe(0)
    expect(region?.querySelectorAll('article')).toHaveLength(2)
    expect(summary?.textContent).toContain('Calm music')
    expect(warning?.textContent).toContain('pool was limited')
    expect(action?.textContent).toContain('Save as playlist')
    expect(region?.contains(summary ?? null)).toBe(false)
    expect(region?.contains(warning ?? null)).toBe(false)
    expect(region?.contains(action ?? null)).toBe(false)
  })

  it('assigns a deliberate stagger order to arriving cards', () => {
    const root = mountComponent(AssistantMessage, { turn: doneTurn(), canSave: true })
    const cards = root.querySelectorAll<HTMLElement>('article')

    expect(cards[0]?.style.getPropertyValue('--card-order')).toBe('0')
    expect(cards[1]?.style.getPropertyValue('--card-order')).toBe('1')
  })

  it('anchors a completed response at the top of the conversation viewport', async () => {
    const scrollIntoView = vi
      .spyOn(Element.prototype, 'scrollIntoView')
      .mockImplementation(() => undefined)
    vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
      callback(0)
      return 1
    })
    const complete = doneTurn()
    const turns = ref<ChatTurn[]>([
      { id: 'user-1', role: 'user', text: 'Focused listening' },
      { ...complete, status: 'streaming', completion: null },
    ])
    const root = document.createElement('div')
    document.body.append(root)
    const app = createApp(
      defineComponent({
        render: () =>
          h(MessageList, {
            turns: turns.value,
            suggestions: [],
            canSave: true,
          }),
      }),
    )
    app.mount(root)
    unmountCallbacks.push(() => {
      app.unmount()
      root.remove()
    })

    turns.value = [turns.value[0] as ChatTurn, complete]
    await nextTick()
    await nextTick()

    expect(scrollIntoView).toHaveBeenCalledWith({ block: 'start' })
  })

  it('keeps suggestions in a labeled focusable row', () => {
    const onPick = vi.fn()
    const suggestions = ['Focus', 'Energy', 'Surprise']
    const root = mountComponent(SuggestionChips, { suggestions, onPick })
    const region = root.querySelector<HTMLElement>('[aria-label="Listening suggestions"]')
    const buttons = root.querySelectorAll<HTMLButtonElement>('button')

    expect(region?.tabIndex).toBe(0)
    expect(buttons).toHaveLength(suggestions.length)
    expect([...buttons].map((button) => button.textContent?.trim())).toEqual(suggestions)

    buttons[0]?.click()
    expect(onPick).toHaveBeenCalledWith('Focus')
  })

  it('renders a first-stage stream error with its code and message', () => {
    const turn: AssistantTurn = {
      ...doneTurn(),
      status: 'error',
      requestId: null,
      intentSummary: '',
      tracks: [],
      warnings: [],
      error: { type: 'error', code: 'first_stage_failed', message: 'Planning failed.' },
      transportFailure: null,
      completion: null,
    }
    const root = mountComponent(AssistantMessage, { turn, canSave: false })
    const alert = root.querySelector<HTMLElement>('[role="alert"]')

    expect(alert?.dataset.errorCode).toBe('first_stage_failed')
    expect(alert?.textContent).toContain('Planning failed.')
  })
})
