import type { HttpClientError } from '@/errors/base'
import { describe, expect, it } from 'vitest'
import { ErrorCode } from '@/errors/codes'
import { classifyResponse } from './error-mapper'

function res(status: number, body: unknown): Response {
  return new Response(typeof body === 'string' ? body : JSON.stringify(body), {
    status,
    headers: { 'content-type': 'application/json' },
  })
}

const req = new Request('https://dify.test/openapi/v1/apps')

function classified(status: number, body: unknown): Promise<HttpClientError> {
  return classifyResponse(req, res(status, body))
}

describe('classifyResponse — canonical ErrorBody', () => {
  it('attaches the parsed body whole as serverError', async () => {
    const body = {
      code: 'invalid_param',
      message: 'Request validation failed',
      status: 422,
      hint: 'check the page parameter',
      details: [{ type: 'int_parsing', loc: ['page'], msg: 'must be >= 1' }],
    }

    const err = await classified(422, body)

    expect(err.serverError).toEqual(body)
    expect(err.message).toBe('Request validation failed')
    expect(err.code).toBe(ErrorCode.Server4xxOther)
  })

  it('401 classifies by status as AuthExpired with CLI login hint', async () => {
    const err = await classified(401, {
      code: 'unauthorized',
      message: 'session expired or revoked',
      status: 401,
    })

    expect(err.code).toBe(ErrorCode.AuthExpired)
    expect(err.hint).toBe("run 'difyctl auth login' to sign in again")
  })

  it('unknown future server code is data, not behavior — status bucket decides', async () => {
    const err = await classified(409, {
      code: 'some_future_code',
      message: 'nope',
      status: 409,
    })

    expect(err.code).toBe(ErrorCode.Server4xxOther)
    expect(err.serverError?.code).toBe('some_future_code')
  })

  it('429 classifies as RateLimited (dedicated exit code) and keeps the server code', async () => {
    const err = await classified(429, {
      code: 'too_many_requests',
      message: 'slow down',
      status: 429,
    })

    expect(err.code).toBe(ErrorCode.RateLimited)
    expect(err.exit()).toBe(7)
    expect(err.serverError?.code).toBe('too_many_requests')
  })

  it('429 with no parseable ErrorBody falls back to a generic rate-limit message', async () => {
    const err = await classified(429, 'not json')

    expect(err.code).toBe(ErrorCode.RateLimited)
    expect(err.serverError).toBeUndefined()
    expect(err.message).toBe('too many requests')
  })
})

describe('classifyResponse 403', () => {
  it('maps 403 to AccessDenied (exit 4 bucket)', async () => {
    const req403 = new Request('https://x/openapi/v1/apps/abc/dsl')
    const res403 = new Response(
      JSON.stringify({
        code: 'unsupported_token_type',
        message: 'unsupported_token_type',
        status: 403,
      }),
      { status: 403, headers: { 'content-type': 'application/json' } },
    )
    const err = await classifyResponse(req403, res403)
    expect(err.code).toBe(ErrorCode.AccessDenied)
    expect(err.message).toBe('unsupported_token_type')
  })

  it('403 with no parseable ErrorBody falls back to generic denied message', async () => {
    const err = await classified(403, 'not json')
    expect(err.code).toBe(ErrorCode.AccessDenied)
    expect(err.message).toBe('not permitted')
  })
})

describe('classifyResponse 426', () => {
  it('maps 426 to VersionSkew (exit 6) and surfaces the server upgrade message', async () => {
    const body = {
      code: 'upgrade_required',
      message: 'difyctl 0.1.0 is no longer supported; upgrade to >= 0.2.0.',
      status: 426,
      hint: 'Upgrade difyctl: https://docs.dify.ai/en/cli/install',
    }

    const err = await classified(426, body)

    expect(err.code).toBe(ErrorCode.VersionSkew)
    expect(err.exit()).toBe(6)
    expect(err.message).toBe('difyctl 0.1.0 is no longer supported; upgrade to >= 0.2.0.')
    expect(err.serverError?.code).toBe('upgrade_required')
  })

  it('426 with no parseable ErrorBody falls back to a version message', async () => {
    const err = await classified(426, 'not json')

    expect(err.code).toBe(ErrorCode.VersionSkew)
    expect(err.message).toBe('client version no longer supported by the server')
  })
})

describe('classifyResponse — non-conforming bodies (no fallback by design)', () => {
  it('non-JSON body yields no serverError, classification by status', async () => {
    const err = await classified(502, '<html>bad gateway</html>')

    expect(err.code).toBe(ErrorCode.Server5xx)
    expect(err.serverError).toBeUndefined()
  })

  it('RFC 8628 string error field yields no serverError and a generic message', async () => {
    const err = await classified(400, { error: 'slow_down' })

    expect(err.message).toBe('request failed (HTTP 400)')
    expect(err.serverError).toBeUndefined()
  })
})
