import { afterEach, describe, expect, it } from 'vitest'

import {
  $composerAttachments,
  $voiceConversationStartRequest,
  addComposerAttachment,
  clearSessionDraft,
  type ComposerAttachment,
  migrateSessionDraft,
  removeComposerAttachment,
  requestVoiceConversationStart,
  SESSION_DRAFTS_STORAGE_KEY,
  stashSessionDraft,
  takeSessionDraft,
  takeVoiceConversationStart,
  updateComposerAttachment
} from './composer'

describe('voice conversation start requests', () => {
  it('latches each request until the main composer consumes it once', () => {
    requestVoiceConversationStart()
    const first = $voiceConversationStartRequest.get()

    expect(takeVoiceConversationStart(first)).toBe(true)
    expect(takeVoiceConversationStart(first)).toBe(false)

    requestVoiceConversationStart()
    expect(takeVoiceConversationStart($voiceConversationStartRequest.get())).toBe(true)
  })
})

function attachment(overrides: Partial<ComposerAttachment> & Pick<ComposerAttachment, 'id'>): ComposerAttachment {
  return { kind: 'file', label: 'doc.pdf', ...overrides }
}

describe('updateComposerAttachment', () => {
  afterEach(() => {
    $composerAttachments.set([])
  })

  it('replaces an existing attachment in place', () => {
    addComposerAttachment(attachment({ id: 'file:a', uploadState: 'uploading' }))

    const updated = updateComposerAttachment(attachment({ id: 'file:a', attachedSessionId: 'sess-1' }))

    expect(updated).toBe(true)
    const current = $composerAttachments.get()
    expect(current).toHaveLength(1)
    expect(current[0]?.attachedSessionId).toBe('sess-1')
    expect(current[0]?.uploadState).toBeUndefined()
  })

  it('does NOT resurrect an attachment the user removed mid-upload', () => {
    // Drop → eager upload starts → user removes the chip → upload resolves.
    // The late success must not re-add the removed attachment.
    addComposerAttachment(attachment({ id: 'file:a', uploadState: 'uploading' }))
    removeComposerAttachment('file:a')

    const updated = updateComposerAttachment(attachment({ id: 'file:a', attachedSessionId: 'sess-1' }))

    expect(updated).toBe(false)
    expect($composerAttachments.get()).toHaveLength(0)
  })
})

describe('session drafts', () => {
  afterEach(() => {
    for (const scope of ['session-a', 'session-b', null]) {
      clearSessionDraft(scope)
    }

    window.localStorage.clear()
  })

  it('keeps drafts isolated per session scope', () => {
    stashSessionDraft('session-a', 'draft a', [])
    stashSessionDraft('session-b', 'draft b', [attachment({ id: 'image:b', kind: 'image' })])

    expect(takeSessionDraft('session-a')).toEqual({ attachments: [], text: 'draft a' })
    expect(takeSessionDraft('session-b').text).toBe('draft b')
    expect(takeSessionDraft('session-b').attachments.map(a => a.id)).toEqual(['image:b'])
  })

  it('scopes the unsaved new-session draft separately from real sessions', () => {
    stashSessionDraft(null, 'new chat draft', [])
    stashSessionDraft('session-a', 'session draft', [])

    expect(takeSessionDraft(null).text).toBe('new chat draft')
    expect(takeSessionDraft(undefined).text).toBe('new chat draft')
    expect(takeSessionDraft('session-a').text).toBe('session draft')
  })

  it('persists draft text (not attachments) to localStorage', () => {
    stashSessionDraft('session-a', 'survives reload', [attachment({ id: 'file:a' })])

    const persisted = JSON.parse(window.localStorage.getItem(SESSION_DRAFTS_STORAGE_KEY) ?? '{}') as Record<
      string,
      string
    >

    expect(persisted['session-a']).toBe('survives reload')
  })

  it('evicts empty drafts instead of leaving stale entries behind', () => {
    stashSessionDraft('session-a', 'saved', [])
    stashSessionDraft('session-a', '   ', [])

    expect(takeSessionDraft('session-a')).toEqual({ attachments: [], text: '' })
  })

  it('clears a stashed draft after an accepted submit', () => {
    stashSessionDraft('session-a', 'sent prompt', [attachment({ id: 'file:a' })])
    clearSessionDraft('session-a')

    expect(takeSessionDraft('session-a')).toEqual({ attachments: [], text: '' })
  })

  it('returns clones so callers cannot mutate the stash', () => {
    stashSessionDraft('session-a', 'draft', [attachment({ id: 'file:a' })])

    const taken = takeSessionDraft('session-a')
    taken.attachments[0]!.label = 'mutated'

    expect(takeSessionDraft('session-a').attachments[0]?.label).toBe('doc.pdf')
  })

  it('migrates a tip-keyed draft onto the post-compression tip', () => {
    const tipBefore = '20260720_062637_ad96b3'
    const tipAfter = '20260720_071049_a28905'

    stashSessionDraft(tipBefore, 'half typed while thinking', [])

    expect(migrateSessionDraft(tipBefore, tipAfter)).toBe(true)
    expect(takeSessionDraft(tipAfter).text).toBe('half typed while thinking')
    expect(takeSessionDraft(tipBefore).text).toBe('')

    clearSessionDraft(tipAfter)
  })

  it('does not overwrite a non-empty destination draft during migration', () => {
    stashSessionDraft('from', 'old tip draft', [])
    stashSessionDraft('to', 'already typed on new tip', [])

    expect(migrateSessionDraft('from', 'to')).toBe(false)
    expect(takeSessionDraft('to').text).toBe('already typed on new tip')
    expect(takeSessionDraft('from').text).toBe('old tip draft')

    clearSessionDraft('from')
    clearSessionDraft('to')
  })
})
