import { assert, test, describe } from 'vitest'
import { Code, ConnectError } from '@connectrpc/connect'
import {
  handleRpcError,
  handleRpcErrorWithHealthCheck,
} from '../../src/envd/rpc'
import {
  AuthenticationError,
  InvalidArgumentError,
  NotFoundError,
  RateLimitError,
  SandboxError,
  TimeoutError,
} from '../../src/errors'

describe('handleRpcError', () => {
  test('returns InvalidArgumentError for InvalidArgument', () => {
    const err = handleRpcError(new ConnectError('bad', Code.InvalidArgument))
    assert.instanceOf(err, InvalidArgumentError)
  })

  test('returns AuthenticationError for Unauthenticated', () => {
    const err = handleRpcError(new ConnectError('nope', Code.Unauthenticated))
    assert.instanceOf(err, AuthenticationError)
  })

  test('returns NotFoundError for NotFound', () => {
    const err = handleRpcError(new ConnectError('missing', Code.NotFound))
    assert.instanceOf(err, NotFoundError)
  })

  test('returns RateLimitError for ResourceExhausted', () => {
    const err = handleRpcError(
      new ConnectError('too many', Code.ResourceExhausted)
    )
    assert.instanceOf(err, RateLimitError)
    assert.include(err.message, 'Rate limit')
  })

  test('returns TimeoutError for Unavailable', () => {
    const err = handleRpcError(new ConnectError('gone', Code.Unavailable))
    assert.instanceOf(err, TimeoutError)
  })

  test('falls back to SandboxError for unmapped code', () => {
    const err = handleRpcError(new ConnectError('boom', Code.Internal))
    assert.instanceOf(err, SandboxError)
  })

  test('falls back to generic SandboxError for Unknown "terminated"', () => {
    const err = handleRpcError(new ConnectError('terminated', Code.Unknown))
    assert.instanceOf(err, SandboxError)
    assert.include(err.message, 'terminated')
    assert.notInclude(err.message, 'killed')
  })

  test('returns the original error when not a ConnectError', () => {
    const original = new Error('not connect')
    const err = handleRpcError(original)
    assert.strictEqual(err, original)
  })
})

describe('handleRpcErrorWithHealthCheck', () => {
  const terminated = () => new ConnectError('terminated', Code.Unknown)

  // Each JS runtime surfaces a dropped connection with different wording
  const runtimeTerminatedMessages = {
    Node: 'terminated',
    Bun: 'The socket connection was closed unexpectedly',
    Deno: 'error reading a body from connection',
  }

  test('returns a TimeoutError when the health check says the sandbox is not running', async () => {
    const err = await handleRpcErrorWithHealthCheck(
      terminated(),
      async () => false
    )
    assert.instanceOf(err, TimeoutError)
    assert.include(err.message, 'sandbox was killed or reached its end of life')
  })

  for (const [runtime, message] of Object.entries(runtimeTerminatedMessages)) {
    test(`treats the ${runtime} dropped-connection message as terminated`, async () => {
      const err = await handleRpcErrorWithHealthCheck(
        new ConnectError(message, Code.Unknown),
        async () => false
      )
      assert.instanceOf(err, TimeoutError)
      assert.include(
        err.message,
        'sandbox was killed or reached its end of life'
      )
    })
  }

  test('falls back to the generic mapping when the health check says the sandbox is running', async () => {
    const err = await handleRpcErrorWithHealthCheck(
      terminated(),
      async () => true
    )
    assert.instanceOf(err, SandboxError)
    assert.notInstanceOf(err, TimeoutError)
    assert.notInclude(err.message, 'killed')
  })

  test('falls back to the generic mapping when the sandbox state is unknown', async () => {
    const err = await handleRpcErrorWithHealthCheck(
      terminated(),
      async () => undefined
    )
    assert.instanceOf(err, SandboxError)
    assert.notInstanceOf(err, TimeoutError)
    assert.notInclude(err.message, 'killed')
  })

  test('falls back to the generic mapping when the health check itself fails', async () => {
    const err = await handleRpcErrorWithHealthCheck(terminated(), async () => {
      throw new Error('health check failed')
    })
    assert.instanceOf(err, SandboxError)
    assert.notInstanceOf(err, TimeoutError)
    assert.notInclude(err.message, 'killed')
  })

  test('does not run the health check for other errors', async () => {
    let called = false
    const err = await handleRpcErrorWithHealthCheck(
      new ConnectError('missing', Code.NotFound),
      async () => {
        called = true
        return false
      }
    )
    assert.instanceOf(err, NotFoundError)
    assert.isFalse(called)
  })
})
