import { describe, expect, it } from 'vitest'
import type { ProviderEntry } from '../../core/model-registry.ts'
import {
  generationUrl,
  isAnthropicNative,
  isGenerationPath,
  rewriteModel,
  stripAnthropicServerToolsFromBody,
  withoutServerTools,
} from './exec.ts'

function encode(value: unknown): ArrayBuffer {
  const u8 = new TextEncoder().encode(typeof value === 'string' ? value : JSON.stringify(value))
  const ab = new ArrayBuffer(u8.byteLength)
  new Uint8Array(ab).set(u8)
  return ab
}

function decode(buf: ArrayBuffer): unknown {
  return JSON.parse(new TextDecoder().decode(buf))
}

describe('generationUrl', () => {
  it('appends the version segment to a host-root base', () => {
    expect(generationUrl('https://api.anthropic.com', 'anthropic-messages')).toBe(
      'https://api.anthropic.com/v1/messages',
    )
    expect(generationUrl('https://api.openai.com', 'openai-chat')).toBe(
      'https://api.openai.com/v1/chat/completions',
    )
    expect(generationUrl('https://api.openai.com', 'openai-responses')).toBe(
      'https://api.openai.com/v1/responses',
    )
  })

  it('does not double the version segment when the base already ends in /v1', () => {
    expect(generationUrl('https://api.xiangxinai.cn/coding/v1', 'openai-chat')).toBe(
      'https://api.xiangxinai.cn/coding/v1/chat/completions',
    )
    expect(generationUrl('https://openrouter.ai/api/v1', 'openai-chat')).toBe(
      'https://openrouter.ai/api/v1/chat/completions',
    )
  })

  it('tolerates a trailing slash', () => {
    expect(generationUrl('https://api.anthropic.com/', 'anthropic-messages')).toBe(
      'https://api.anthropic.com/v1/messages',
    )
    expect(generationUrl('https://api.openai.com/v1/', 'openai-chat')).toBe(
      'https://api.openai.com/v1/chat/completions',
    )
  })

  it('omits the /v1 segment on codex chatgpt.com backend', () => {
    // chatgpt.com/backend-api/codex serves /responses directly; the codex
    // CLI hits this path natively. Without this exception, off-agent
    // routes (openclaw → codex subscription) build /v1/responses and the
    // upstream replies with a 403 HTML login page.
    expect(generationUrl('https://chatgpt.com/backend-api/codex', 'openai-responses')).toBe(
      'https://chatgpt.com/backend-api/codex/responses',
    )
    expect(generationUrl('https://chatgpt.com/backend-api/codex/', 'openai-responses')).toBe(
      'https://chatgpt.com/backend-api/codex/responses',
    )
  })
})

describe('isGenerationPath', () => {
  it('matches each API generation endpoint', () => {
    expect(isGenerationPath('anthropic-messages', '/v1/messages')).toBe(true)
    expect(isGenerationPath('openai-chat', '/v1/chat/completions')).toBe(true)
    expect(isGenerationPath('openai-chat', '/chat/completions')).toBe(true)
    expect(isGenerationPath('openai-responses', '/v1/responses')).toBe(true)
  })

  it('rejects non-generation and mismatched paths', () => {
    expect(isGenerationPath('anthropic-messages', '/v1/models')).toBe(false)
    expect(isGenerationPath('openai-chat', '/v1/messages')).toBe(false)
    expect(isGenerationPath('openai-responses', '/v1/chat/completions')).toBe(false)
  })
})

describe('rewriteModel', () => {
  it('replaces the top-level model field', () => {
    const out = rewriteModel(encode({ model: 'old', messages: [] }), 'new-model')
    expect(decode(out)).toEqual({ model: 'new-model', messages: [] })
  })

  it('adds a model field when absent', () => {
    const out = rewriteModel(encode({ messages: [] }), 'new-model')
    expect(decode(out)).toEqual({ messages: [], model: 'new-model' })
  })

  it('forwards an unparseable body unchanged', () => {
    const buf = encode('not json{')
    expect(rewriteModel(buf, 'new-model')).toBe(buf)
  })

  it('forwards a non-object body unchanged', () => {
    const buf = encode([1, 2, 3])
    expect(rewriteModel(buf, 'new-model')).toBe(buf)
  })
})

function provider(baseUrl: string): ProviderEntry {
  return {
    id: 'p',
    label: 'p',
    baseUrl,
    api: 'anthropic-messages',
    auth: { kind: 'passthrough' },
    origin: 'manual',
  }
}

describe('isAnthropicNative', () => {
  it('recognizes api.anthropic.com and *.anthropic.com', () => {
    expect(isAnthropicNative(provider('https://api.anthropic.com'))).toBe(true)
    expect(isAnthropicNative(provider('https://api.anthropic.com/v1'))).toBe(true)
    expect(isAnthropicNative(provider('https://shadow.anthropic.com'))).toBe(true)
  })

  it('rejects third-party Anthropic-compatible endpoints', () => {
    expect(isAnthropicNative(provider('https://api.deepseek.com/anthropic'))).toBe(false)
    expect(isAnthropicNative(provider('https://api.example.com/'))).toBe(false)
  })

  it('returns false for a malformed baseUrl', () => {
    expect(isAnthropicNative(provider('not a url'))).toBe(false)
  })
})

describe('withoutServerTools', () => {
  const serverTool = { type: 'web_search_20250305', name: 'web_search' }
  const customTool = { name: 'my_tool', input_schema: { type: 'object' } }
  const mcpTool = { name: 'mcp__thomas__web_search', input_schema: { type: 'object' } }

  it('drops Anthropic server tools, keeps custom + mcp tools', () => {
    const body = { messages: [], tools: [serverTool, customTool, mcpTool] }
    const out = withoutServerTools(body)
    expect(out).not.toBe(body)
    expect(out.tools).toEqual([customTool, mcpTool])
  })

  it('returns the same reference when there is nothing to strip', () => {
    const body = { messages: [], tools: [customTool] }
    expect(withoutServerTools(body)).toBe(body)
  })

  it('removes the tools key entirely when every entry was a server tool', () => {
    const body = { messages: [], tools: [serverTool] }
    const out = withoutServerTools(body)
    expect('tools' in out).toBe(false)
  })

  it('treats type:"custom" as a custom tool (not stripped)', () => {
    const c = { type: 'custom', name: 'x', input_schema: { type: 'object' } }
    const body = { tools: [c] }
    expect(withoutServerTools(body)).toBe(body)
  })

  it('leaves bodies without tools unchanged', () => {
    const body = { messages: [] }
    expect(withoutServerTools(body)).toBe(body)
  })
})

describe('stripAnthropicServerToolsFromBody', () => {
  it('round-trips through bytes and strips server tools', () => {
    const body = encode({
      model: 'claude-x',
      tools: [
        { type: 'web_search_20250305', name: 'web_search' },
        { name: 'my_tool', input_schema: {} },
      ],
    })
    const out = stripAnthropicServerToolsFromBody(body)
    const obj = decode(out) as { tools: Array<{ name: string }> }
    expect(obj.tools).toHaveLength(1)
    expect(obj.tools[0]?.name).toBe('my_tool')
  })

  it('returns the same buffer when nothing to strip', () => {
    const body = encode({ tools: [{ name: 'x', input_schema: {} }] })
    expect(stripAnthropicServerToolsFromBody(body)).toBe(body)
  })

  it('returns the same buffer for an unparseable body', () => {
    const body = encode('not json{')
    expect(stripAnthropicServerToolsFromBody(body)).toBe(body)
  })
})
