import { describe, expect, it, vi } from 'vitest'
import { encodeBase64UrlBytes } from 'relay-contracts'
import { readConfig } from '../src/config'
import { createApiClient, type ApiClient, type ApiClientV08 } from '../src/api-client'
import { ApiError } from '../src/errors'

const cfg = readConfig({
  protocol: 'https:',
  host: 'alice.term.example.com',
  hostname: 'alice.term.example.com',
})

const validHost = {
  hostId: '11111111-1111-4111-8111-111111111111',
  accountId: '22222222-2222-4222-8222-222222222222',
  subdomain: 'alice',
  agentPubkey: encodeBase64UrlBytes(new Uint8Array([1, 2, 3, 4])),
  enrollFpr: 'fpr-abc',
  status: 'online',
  lastSeen: '2026-01-01T00:00:00Z',
  createdAt: '2026-01-01T00:00:00Z',
  revokedAt: null,
}

/** Build a mock fetch returning `body` as JSON with the given status. Records every call. */
function mockFetch(body: unknown, status = 200): { fetch: typeof fetch; calls: Request[] } {
  const calls: Request[] = []
  const fn = vi.fn(async (url: string, init?: RequestInit) => {
    calls.push(new Request(`https://alice.term.example.com${url}`, init))
    return {
      ok: status >= 200 && status < 300,
      status,
      json: async () => body,
    } as Response
  })
  return { fetch: fn as unknown as typeof fetch, calls }
}

describe('createApiClient — Zod validation + INV3 (T2)', () => {
  it('listHosts sends NO account_id/tenant_id in url, query, or body (INV3)', async () => {
    const { fetch, calls } = mockFetch([validHost])
    const api = createApiClient(cfg, fetch)
    const hosts = await api.listHosts()
    expect(hosts).toHaveLength(1)
    expect(hosts[0]?.enrollFpr).toBe('fpr-abc')
    expect(hosts[0]?.agentPubkey).toBeInstanceOf(Uint8Array)

    const req = calls[0]!
    expect(req.url).not.toMatch(/account_id|tenant_id/i)
    const body = await req.text()
    expect(body).not.toMatch(/account_id|tenant_id/i)
  })

  it('requestPairingCode posts an empty body — account derived server-side (INV3)', async () => {
    const { fetch, calls } = mockFetch({ code: 'ABCD-1234', expiresAt: '2026-01-01T00:02:00Z' })
    const api = createApiClient(cfg, fetch)
    const out = await api.requestPairingCode()
    expect(out.code).toBe('ABCD-1234')
    const body = await calls[0]!.text()
    expect(body).not.toMatch(/account_id|tenant_id/i)
    expect(JSON.parse(body)).toEqual({})
  })

  it('getHost returns a Zod-validated HostRecord with a non-empty enrollFpr (T8 trust root)', async () => {
    const { fetch } = mockFetch(validHost)
    const api = createApiClient(cfg, fetch)
    const host = await api.getHost('11111111-1111-4111-8111-111111111111')
    expect(host.enrollFpr).toBe('fpr-abc')
  })

  it('throws ApiError(parse) on a malformed host (missing subdomain) — never a torn object', async () => {
    const bad = { ...validHost, subdomain: undefined }
    const { fetch } = mockFetch(bad)
    const api = createApiClient(cfg, fetch)
    await expect(api.getHost('h1')).rejects.toBeInstanceOf(ApiError)
    await expect(api.getHost('h1')).rejects.toMatchObject({ kind: 'parse' })
  })

  it('throws ApiError(parse) when enrollFpr is empty (no relay-forwarded fallback for T8)', async () => {
    const { fetch } = mockFetch({ ...validHost, enrollFpr: '' })
    const api = createApiClient(cfg, fetch)
    await expect(api.getHost('h1')).rejects.toMatchObject({ kind: 'parse' })
  })

  it('maps 401 → ApiError(unauthenticated) (surfaces re-login, no silent swallow)', async () => {
    const { fetch } = mockFetch({}, 401)
    const api = createApiClient(cfg, fetch)
    await expect(api.listHosts()).rejects.toMatchObject({ kind: 'unauthenticated' })
  })

  it('maps 403 → ApiError(forbidden) (INV1 enforced upstream, respected here)', async () => {
    const { fetch } = mockFetch({}, 403)
    const api = createApiClient(cfg, fetch)
    await expect(api.getHost('foreign')).rejects.toMatchObject({ kind: 'forbidden' })
  })

  it('v0.9+ issueCapabilityToken returns the opaque raw token', async () => {
    const { fetch } = mockFetch({ token: 'v2.public.rawtoken' })
    const api = createApiClient(cfg, fetch)
    const token = await api.issueCapabilityToken('h1', ['attach'])
    expect(token).toBe('v2.public.rawtoken')
  })

  it('v0.8 phasing: issueCapabilityToken is ABSENT from ApiClientV08 (structural guard)', () => {
    const { fetch } = mockFetch([validHost])
    const client: ApiClient = createApiClient(cfg, fetch)
    const v08: ApiClientV08 = client
    // @ts-expect-error — issueCapabilityToken is v0.9+, not on the v0.8 surface a v0.8 build uses.
    void v08.issueCapabilityToken
  })
})
