#!/usr/bin/env node
// Bota Standard Provider API - standalone conformance smoke-test (zero deps).
//
//   BASE_URL=https://api.your-rms.com/bota API_KEY=your-token node bota-conformance.mjs
//   # include the booking lifecycle (creates + cancels a real reservation):
//   CONFORMANCE_BOOKINGS=1 BASE_URL=... API_KEY=... node bota-conformance.mjs
//
// This checks response SHAPES (required fields). For authoritative validation
// against Bota's exact runtime schemas, use "Connect & verify" in the panel.

const BASE = (process.env.BASE_URL || '').replace(/\/+$/, '')
const KEY = process.env.API_KEY || ''
const DO_BOOK = process.env.CONFORMANCE_BOOKINGS === '1'
if (!BASE) { console.error('Set BASE_URL (and usually API_KEY).'); process.exit(2) }

const results = []
const isNum = (v) => typeof v === 'number' && Number.isFinite(v)
const isStr = (v) => typeof v === 'string' && v.length > 0
function need(obj, fields, where) {
  for (const f of fields) {
    const parts = f.split(':'); const k = parts[0]; const kind = parts[1]
    const v = obj == null ? undefined : obj[k]
    if (v == null) throw new Error(where + ': missing "' + k + '"')
    if (kind === 'n' && !isNum(v)) throw new Error(where + ': "' + k + '" must be a number')
    if (kind === 's' && !isStr(v)) throw new Error(where + ': "' + k + '" must be a string')
  }
}
async function req(method, path, body, idemKey) {
  const headers = { 'Content-Type': 'application/json' }
  if (KEY) headers.Authorization = 'Bearer ' + KEY
  if (idemKey) headers['Idempotency-Key'] = idemKey
  const res = await fetch(BASE + path, {
    method, headers, body: body ? JSON.stringify(body) : undefined, redirect: 'manual',
  })
  if (res.status >= 300 && res.status < 400) throw new Error(res.status + ' redirect - return absolute final URLs')
  const text = await res.text()
  if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + text.slice(0, 160))
  try { return JSON.parse(text) } catch { throw new Error('response is not JSON: ' + text.slice(0, 120)) }
}
async function check(name, fn) {
  try { await fn(); results.push({ name, ok: true }) }
  catch (e) { results.push({ name, ok: false, detail: (e && e.message) || String(e) }) }
}
const inDays = (n) => new Date(Date.now() + n * 86400000).toISOString().slice(0, 10)

async function main() {
  let officeId = '', tariffId = '', groupId = '', rateRef = '', total = 0

  await check('GET /health', async () => {
    const d = await req('GET', '/health'); if (!d || d.ok !== true) throw new Error('expected { ok: true }')
  })
  await check('GET /offices', async () => {
    const d = await req('GET', '/offices'); const o = (d.offices || [])[0]
    if (!o) throw new Error('no offices returned'); need(o, ['id:s', 'code:s', 'name:s'], 'offices[0]'); officeId = o.id
  })
  await check('POST /availability', async () => {
    if (!officeId) throw new Error('skipped - no office from /offices')
    const d = await req('POST', '/availability', { officeId, pickupDate: inDays(30), dropoffDate: inDays(33), pickupTime: '10:00', dropoffTime: '10:00', driverAge: 30 })
    const f = (d.offers || [])[0]
    if (f) { need(f, ['groupId:s', 'tariffId:s', 'totalPriceCents:n', 'pricePerDayCents:n', 'currency:s'], 'offers[0]'); if (f.officeId) officeId = f.officeId; tariffId = f.tariffId; groupId = f.groupId; rateRef = f.rateRef || ''; total = f.totalPriceCents }
  })
  await check('GET /extras', async () => {
    if (!tariffId) throw new Error('skipped - no tariff'); const d = await req('GET', '/extras?tariffId=' + encodeURIComponent(tariffId))
    for (const e of (d.extras || [])) {
      need(e, ['id:s', 'code:s', 'name:s'], 'extras[]')
      // Spec 1.5.0 coverage-tier fields are optional, but if present they
      // must be well-typed or Bota's runtime validation will reject them.
      if (e.insuranceKind != null && !isStr(e.insuranceKind)) throw new Error('extras[]: "insuranceKind" must be a string')
      if (e.deductibleCents != null && !isNum(e.deductibleCents)) throw new Error('extras[]: "deductibleCents" must be a number')
      if (e.depositCents != null && !isNum(e.depositCents)) throw new Error('extras[]: "depositCents" must be a number')
      if (e.depositReductionCents != null && !isNum(e.depositReductionCents)) throw new Error('extras[]: "depositReductionCents" must be a number')
    }
  })
  await check('GET /concepts (optional)', async () => {
    if (!tariffId) throw new Error('skipped - no tariff'); const d = await req('GET', '/concepts?tariffId=' + encodeURIComponent(tariffId))
    if (!Array.isArray(d.sellable)) throw new Error('expected sellable[]')
  })
  await check('POST /rate-rules (optional)', async () => {
    if (!tariffId) throw new Error('skipped - no tariff'); const d = await req('POST', '/rate-rules', { tariffIds: [tariffId] })
    if (!d || typeof d.rateRules !== 'object') throw new Error('expected rateRules{}')
  })

  if (DO_BOOK) {
    let bookingId = ''
    await check('POST /bookings', async () => {
      const d = await req('POST', '/bookings', { officeId, pickupDate: inDays(30), dropoffDate: inDays(33), groupId, tariffId, rateRef, customer: { givenName: 'Conformance', surname: 'Test', email: 'conformance@example.com' }, expectedTotalCents: total }, 'conformance-' + Date.now())
      if (typeof d.bookingId !== 'string') throw new Error('BookingResult: bookingId must be a string')
      if (['confirmed', 'reprice_required'].indexOf(d.status) < 0) throw new Error('unexpected status ' + d.status)
      need(d, ['totalCents:n', 'currency:s'], 'BookingResult')
      if (d.status === 'confirmed' && !isStr(d.bookingId)) throw new Error('confirmed booking needs a bookingId')
      bookingId = d.bookingId
    })
    await check('GET /bookings/{id}', async () => {
      if (!bookingId) throw new Error('skipped - no bookingId'); need(await req('GET', '/bookings/' + encodeURIComponent(bookingId)), ['bookingId:s', 'status:s', 'totalCents:n'], 'BookingDetail')
    })
    await check('POST /bookings/{id}/cancel', async () => {
      if (!bookingId) throw new Error('skipped - no bookingId'); await req('POST', '/bookings/' + encodeURIComponent(bookingId) + '/cancel', { reason: 'conformance' })
    })
  }

  let failed = 0
  for (const r of results) {
    const optional = r.name.indexOf('optional') >= 0
    const mark = r.ok ? '\u2713' : optional ? '\u25cb' : '\u2717'
    if (!r.ok && !optional) failed++
    console.log(mark + ' ' + r.name + (r.detail ? ' - ' + r.detail : ''))
  }
  console.log(failed === 0
    ? '\nPASS - smoke test green. Use "Connect & verify" in Bota for authoritative validation.'
    : '\nFAIL - ' + failed + ' required check(s) failed.')
  process.exit(failed === 0 ? 0 : 1)
}
main().catch((e) => { console.error(e); process.exit(1) })
