import { describe, it, expect, vi, beforeEach } from 'vitest'

const cmsApiMock = vi.fn()
vi.mock('../../lib/api.js', () => ({
  cmsApi: (...args: unknown[]) => cmsApiMock(...args),
}))

const {
  coauthorText,
  coauthorActionLabel,
  coauthorActionDescription,
  isCoauthorAction,
  COAUTHOR_ACTIONS,
  COAUTHOR_TEXT_MAX_CHARS,
} = await import('../../lib/coauthor-client.js')

beforeEach(() => {
  cmsApiMock.mockReset()
})

describe('isCoauthorAction', () => {
  it('accepts the four canonical actions', () => {
    expect(isCoauthorAction('rewrite')).toBe(true)
    expect(isCoauthorAction('expand')).toBe(true)
    expect(isCoauthorAction('compress')).toBe(true)
    expect(isCoauthorAction('proofread')).toBe(true)
  })

  it('rejects anything else', () => {
    expect(isCoauthorAction('summarize')).toBe(false)
    expect(isCoauthorAction('')).toBe(false)
    expect(isCoauthorAction(null)).toBe(false)
    expect(isCoauthorAction(undefined)).toBe(false)
    expect(isCoauthorAction(42)).toBe(false)
  })
})

describe('COAUTHOR_ACTIONS', () => {
  it('contains exactly the four canonical actions', () => {
    expect([...COAUTHOR_ACTIONS]).toEqual(['rewrite', 'expand', 'compress', 'proofread'])
  })
})

describe('coauthorActionLabel + coauthorActionDescription', () => {
  it('returns a non-empty label and description for each action', () => {
    for (const action of COAUTHOR_ACTIONS) {
      expect(coauthorActionLabel(action)).toMatch(/\S/)
      expect(coauthorActionDescription(action)).toMatch(/\S/)
    }
  })
})

describe('coauthorText — input validation', () => {
  it('returns an error when text is empty', async () => {
    const out = await coauthorText('rewrite', '')
    expect(out).toEqual({ ok: false, error: 'No text provided', status: 0 })
    expect(cmsApiMock).not.toHaveBeenCalled()
  })

  it('returns an error when text is only whitespace', async () => {
    const out = await coauthorText('rewrite', '   \n\t ')
    expect(out).toEqual({ ok: false, error: 'No text provided', status: 0 })
    expect(cmsApiMock).not.toHaveBeenCalled()
  })

  it('returns an error when text exceeds the client-side ceiling', async () => {
    const huge = 'a'.repeat(COAUTHOR_TEXT_MAX_CHARS + 1)
    const out = await coauthorText('rewrite', huge)
    expect(out.ok).toBe(false)
    if (!out.ok) {
      expect(out.error).toMatch(/exceeds/)
      expect(out.status).toBe(0)
    }
    expect(cmsApiMock).not.toHaveBeenCalled()
  })

  it('returns an error for an unknown action', async () => {
    // @ts-expect-error — exercising the runtime guard
    const out = await coauthorText('summarize', 'hello world')
    expect(out.ok).toBe(false)
    if (!out.ok) {
      expect(out.error).toMatch(/Unknown action/)
    }
    expect(cmsApiMock).not.toHaveBeenCalled()
  })
})

describe('coauthorText — request shape', () => {
  it('POSTs to /ai/coauthor with the action + text body', async () => {
    cmsApiMock.mockResolvedValue({ data: { action: 'rewrite', text: 'output' }, status: 200 })

    await coauthorText('rewrite', 'hello')

    expect(cmsApiMock).toHaveBeenCalledTimes(1)
    const [path, init] = cmsApiMock.mock.calls[0]!
    expect(path).toBe('/ai/coauthor')
    expect(init.method).toBe('POST')
    expect(JSON.parse(init.body)).toEqual({ action: 'rewrite', text: 'hello' })
  })

  it('forwards style/tone/instructions/targetLength when provided', async () => {
    cmsApiMock.mockResolvedValue({ data: { action: 'expand', text: 'longer output' }, status: 200 })

    await coauthorText('expand', 'seed', {
      style: 'academic',
      tone: 'formal',
      instructions: 'add two examples',
      targetLength: 120,
    })

    const init = cmsApiMock.mock.calls[0]![1]
    expect(JSON.parse(init.body)).toEqual({
      action: 'expand',
      text: 'seed',
      style: 'academic',
      tone: 'formal',
      instructions: 'add two examples',
      targetLength: 120,
    })
  })

  it('omits undefined options from the body', async () => {
    cmsApiMock.mockResolvedValue({ data: { action: 'rewrite', text: 'out' }, status: 200 })

    await coauthorText('rewrite', 'in', { style: undefined, tone: undefined })

    const init = cmsApiMock.mock.calls[0]![1]
    const body = JSON.parse(init.body)
    expect(body).toEqual({ action: 'rewrite', text: 'in' })
    expect('style' in body).toBe(false)
    expect('tone' in body).toBe(false)
  })
})

describe('coauthorText — response handling', () => {
  it('returns the parsed result on a successful response', async () => {
    cmsApiMock.mockResolvedValue({
      data: { action: 'rewrite', text: 'rewritten' },
      status: 200,
    })

    const out = await coauthorText('rewrite', 'original')
    expect(out).toEqual({
      ok: true,
      result: { action: 'rewrite', text: 'rewritten' },
    })
  })

  it('forwards proofread changes only when action is proofread', async () => {
    const changes = [{ original: 'teh', corrected: 'the' }]
    cmsApiMock.mockResolvedValue({
      data: { action: 'proofread', text: 'the cat', changes },
      status: 200,
    })

    const out = await coauthorText('proofread', 'teh cat')
    expect(out.ok).toBe(true)
    if (out.ok) {
      expect(out.result.changes).toEqual(changes)
    }
  })

  it('drops changes when the action is not proofread', async () => {
    cmsApiMock.mockResolvedValue({
      data: { action: 'rewrite', text: 'rewritten', changes: [{ original: 'a', corrected: 'b' }] },
      status: 200,
    })

    const out = await coauthorText('rewrite', 'a')
    expect(out.ok).toBe(true)
    if (out.ok) {
      expect(out.result.changes).toBeUndefined()
    }
  })

  it('returns an error when cmsApi reports an error', async () => {
    cmsApiMock.mockResolvedValue({ error: 'rate limited', status: 429 })

    const out = await coauthorText('rewrite', 'x')
    expect(out).toEqual({ ok: false, error: 'rate limited', status: 429 })
  })

  it('synthesizes an error message when none is provided', async () => {
    cmsApiMock.mockResolvedValue({ status: 500 })

    const out = await coauthorText('rewrite', 'x')
    expect(out.ok).toBe(false)
    if (!out.ok) {
      expect(out.error).toMatch(/500/)
      expect(out.status).toBe(500)
    }
  })

  it('flags malformed responses missing a text field', async () => {
    cmsApiMock.mockResolvedValue({ data: { action: 'rewrite' }, status: 200 })

    const out = await coauthorText('rewrite', 'x')
    expect(out.ok).toBe(false)
    if (!out.ok) {
      expect(out.error).toMatch(/Malformed/)
    }
  })

  it('falls back to the requested action when the server omits it', async () => {
    cmsApiMock.mockResolvedValue({ data: { text: 'out' }, status: 200 })

    const out = await coauthorText('compress', 'in')
    expect(out.ok).toBe(true)
    if (out.ok) {
      expect(out.result.action).toBe('compress')
    }
  })
})
