import { describe, test, expect, vi } from 'vitest'
import { APP_SUBPROTOCOL, encodeTokenSubprotocol } from 'relay-contracts'
import {
  authorizeUpgrade,
  extractCapabilityToken,
  TOKEN_COOKIE_NAME,
  type UpgradeRequest,
  type AuthorizeDeps,
} from '../data-plane/upgrade.js'
import type { Authorizer, AuthzOutcome } from '../data-plane/authz-port.js'
import type { RouteResolver, ResolvedHost } from '../data-plane/subdomain-router.js'

const RAW_TOKEN = 'cap-token-abc123'
const RESOLVED: ResolvedHost = { hostId: 'host-uuid-1', accountId: 'acct-1', subdomain: 'alice' }

function resolver(overrides?: Partial<Record<string, ResolvedHost | null>>): RouteResolver {
  return {
    resolveSubdomain: async (sub) => {
      if (overrides && sub in overrides) return overrides[sub] ?? null
      return sub === 'alice' ? RESOLVED : null
    },
  }
}

function okAuthorizer(): Authorizer {
  const ok: AuthzOutcome = { ok: true, hostId: RESOLVED.hostId, principal: 'p-1', jti: 'jti-9' }
  return { onUpgrade: vi.fn(async () => ok), onReattach: vi.fn(async () => ok) }
}

function baseReq(over?: Partial<UpgradeRequest>): UpgradeRequest {
  return {
    host: 'alice.term.example.com',
    origin: 'https://alice.term.example.com',
    url: '/term?join=x',
    subprotocols: [APP_SUBPROTOCOL, encodeTokenSubprotocol(RAW_TOKEN)],
    cookies: {},
    remoteAddr: '203.0.113.7',
    dpop: { proof: 'proof', publicKeyThumbprint: 'jkt' },
    activeSessionCount: 2,
    ...over,
  }
}

function deps(authorizer: Authorizer, r: RouteResolver = resolver()): AuthorizeDeps {
  return {
    authorizer,
    resolver: r,
    baseDomain: 'term.example.com',
    now: () => 1000,
    remoteAddrSalt: 'salt',
    requiredRight: 'attach',
  }
}

describe('authorizeUpgrade — thin adapter over P5 (T8, FIX 6a)', () => {
  test('happy: delegates to onUpgrade with a fully-populated ctx; maps to {ok:true}', async () => {
    const auth = okAuthorizer()
    const d = deps(auth)
    const res = await authorizeUpgrade(baseReq(), d)
    expect(res.ok).toBe(true)
    if (!res.ok) return
    expect(res.hostId).toBe(RESOLVED.hostId)
    expect(res.open.subdomain).toBe('alice')
    expect(res.open.capabilityTokenRef).toBe('jti-9')
    expect(res.acceptedSubprotocol).toBe(APP_SUBPROTOCOL)
    expect(auth.onUpgrade).toHaveBeenCalledTimes(1)
    const ctx = (auth.onUpgrade as ReturnType<typeof vi.fn>).mock.calls[0]![0]
    expect(ctx.expectedAud).toBe('alice')
    expect(ctx.requestedHostId).toBe(RESOLVED.hostId)
    expect(ctx.requiredRight).toBe('attach')
    expect(ctx.capabilityRaw).toBe(RAW_TOKEN) // decoded from the subprotocol carrier
    expect(ctx.dpop).toEqual({ proof: 'proof', publicKeyThumbprint: 'jkt' })
    expect(ctx.activeSessionCount).toBe(2)
    expect(ctx.principal).toBeNull() // P5 resolves identity from the signed token (INV3)
  })

  test('P5 deny is passed through verbatim; no independent allow branch', async () => {
    const deny: AuthzOutcome = { ok: false, status: 401 }
    const auth: Authorizer = { onUpgrade: vi.fn(async () => deny), onReattach: vi.fn(async () => deny) }
    const res = await authorizeUpgrade(baseReq(), deps(auth))
    expect(res).toEqual({ ok: false, status: 401 })
  })

  test('reattach: sessionId present → onReattach is called (not onUpgrade)', async () => {
    const auth = okAuthorizer()
    await authorizeUpgrade(baseReq({ sessionId: 'sess-1' }), deps(auth))
    expect(auth.onReattach).toHaveBeenCalledTimes(1)
    expect(auth.onUpgrade).not.toHaveBeenCalled()
    const ctx = (auth.onReattach as ReturnType<typeof vi.fn>).mock.calls[0]![0]
    expect(ctx.sessionId).toBe('sess-1')
  })

  test('token via cookie fallback is accepted', async () => {
    const auth = okAuthorizer()
    const req = baseReq({ subprotocols: [APP_SUBPROTOCOL], cookies: { [TOKEN_COOKIE_NAME]: RAW_TOKEN } })
    const res = await authorizeUpgrade(req, deps(auth))
    expect(res.ok).toBe(true)
    const via = extractCapabilityToken(req)
    expect(via).toEqual({ token: RAW_TOKEN, via: 'cookie' })
  })

  test('INV15: a token ONLY in the query string → 401 and the authorizer is NEVER called', async () => {
    const auth = okAuthorizer()
    const req = baseReq({ url: `/term?cap=${RAW_TOKEN}`, subprotocols: [APP_SUBPROTOCOL], cookies: {} })
    const res = await authorizeUpgrade(req, deps(auth))
    expect(res).toEqual({ ok: false, status: 401 })
    expect(auth.onUpgrade).not.toHaveBeenCalled()
    expect(extractCapabilityToken(req)).toBeNull() // never reads req.url
  })

  test('local parse: unparseable Host → 401 without calling the authorizer', async () => {
    const auth = okAuthorizer()
    const res = await authorizeUpgrade(baseReq({ host: 'term.example.com' }), deps(auth))
    expect(res).toEqual({ ok: false, status: 401 })
    expect(auth.onUpgrade).not.toHaveBeenCalled()
  })

  test('local parse: unknown subdomain (resolver null) → 403 without calling the authorizer', async () => {
    const auth = okAuthorizer()
    const res = await authorizeUpgrade(baseReq({ host: 'bob.term.example.com' }), deps(auth))
    expect(res).toEqual({ ok: false, status: 403 })
    expect(auth.onUpgrade).not.toHaveBeenCalled()
  })
})
