import { describe, expect, it } from 'vitest'
import {
  AllModelsFailedError,
  AuthError,
  RateLimitError,
  TransientError,
  executeWithFallback,
  withTimeout,
  type AIAdapter,
  type AIRequest,
  type AIResponse,
} from '@platform-modules/ai'

const baseReq: AIRequest = {
  model: 'gate-model',
  messages: [{ role: 'user', content: 'ping' }],
}

function rateLimitAdapter(): AIAdapter {
  return {
    provider: 'always-429',
    async call(): Promise<AIResponse> {
      throw new RateLimitError('429 rate limited', 'always-429')
    },
  }
}

function okAdapter(): AIAdapter {
  return {
    provider: 'ok',
    async call(): Promise<AIResponse> {
      return {
        content: 'ok',
        model: 'gate-model',
        provider: 'ok',
        usage: { promptTokens: 10, completionTokens: 5 },
      }
    },
  }
}

function authAdapter(): AIAdapter {
  return {
    provider: 'auth-fail',
    async call(): Promise<AIResponse> {
      throw new AuthError('401 unauthorized', 'auth-fail')
    },
  }
}

function slowAdapter(delayMs: number): AIAdapter {
  return {
    provider: 'slow',
    async call(): Promise<AIResponse> {
      await new Promise((resolve) => setTimeout(resolve, delayMs))
      return {
        content: 'slow',
        model: 'gate-model',
        provider: 'slow',
        usage: { promptTokens: 1, completionTokens: 1 },
      }
    },
  }
}

const COST_KEYS = ['cost', 'price', 'credits', 'costUsd', 'neurons'] as const

function assertNoCostField(usage: AIResponse['usage']): void {
  expect(usage.promptTokens).toBeTypeOf('number')
  expect(usage.completionTokens).toBeTypeOf('number')
  for (const key of COST_KEYS) {
    expect(key in usage).toBe(false)
  }
  expect(Object.keys(usage).sort()).toEqual(['completionTokens', 'promptTokens'])
}

describe('ai consumer fixture (Gate 3)', () => {
  it('executeWithFallback falls through 429-retryable adapter to OK adapter', async () => {
    const result = await executeWithFallback(baseReq, [
      { adapter: rateLimitAdapter() },
      { adapter: okAdapter() },
    ])
    expect(result.content).toBe('ok')
    assertNoCostField(result.usage)
  })

  it('surfaces AuthError immediately without fall-through', async () => {
    await expect(
      executeWithFallback(baseReq, [
        { adapter: authAdapter() },
        { adapter: okAdapter() },
      ]),
    ).rejects.toBeInstanceOf(AuthError)
  })

  it('throws when fallback chain is exhausted', async () => {
    await expect(
      executeWithFallback(baseReq, [{ adapter: rateLimitAdapter() }]),
    ).rejects.toBeInstanceOf(AllModelsFailedError)
  })

  it('falls through retryable timeout from withTimeout', async () => {
    const result = await executeWithFallback(
      baseReq,
      [{ adapter: slowAdapter(40) }, { adapter: okAdapter() }],
      { timeoutMs: 5 },
    )
    expect(result.content).toBe('ok')
    assertNoCostField(result.usage)
  })

  it('withTimeout raises retryable TransientError', async () => {
    await expect(withTimeout(new Promise(() => {}), 5)).rejects.toBeInstanceOf(TransientError)
  })

  it('AIResponse.usage has token counts and no cost field (boundary guard)', async () => {
    const result = await executeWithFallback(baseReq, [{ adapter: okAdapter() }])
    assertNoCostField(result.usage)
  })
})
