/**
 * Step-handlers для documents-edge. Самые опасные edge-кейсы документов:
 *   - oversell-protection (stock не уходит в минус),
 *   - immutability проведённых документов,
 *   - валидация платежа,
 *   - уникальность штрихкодов per-tenant.
 *
 * Любая утечка/обход — критический баг.
 */
import { login, makeClient } from '../lib/api.js'
import { countRows, psql } from '../lib/db.js'
import { generateEan13 } from '../lib/barcode.js'
import type { CheckResult, Step, Report } from '../lib/report.js'

const TS = Date.now()

interface Ctx {
  apiOnly: boolean
  superAdminToken?: string
  orgId?: string
  adminToken?: string
  unitId?: string
  groupId?: string
  currencyId?: string
  retailPriceTypeId?: string
  storeId?: string
  retailPointId?: string
  supplierId?: string
  productId?: string
  productBarcode?: string
  supplyId?: string
}
interface StepCtx { ctx: Ctx; step: Step; report: Report }

function check(step: Step, c: CheckResult) { step.checks.push(c) }
function asString(x: unknown): string {
  if (x == null) return ''
  if (typeof x === 'string') return x
  try { return JSON.stringify(x).slice(0, 220) } catch { return String(x) }
}

async function ensureSuperAdmin(ctx: Ctx): Promise<string> {
  if (!ctx.superAdminToken) {
    const sa = await login('admin@food-market.local', 'Admin12345!')
    ctx.superAdminToken = sa.accessToken
  }
  return ctx.superAdminToken
}

// ---------------------------------------------------------------------------

export async function step01_bootstrap({ ctx, step, report }: StepCtx) {
  const sa = await ensureSuperAdmin(ctx)
  const saApi = makeClient(sa)

  // 1. Создаём орг + админа.
  const orgRes = await saApi.post('/api/super-admin/organizations', {
    org: {
      name: `Edge Shop ${TS}`, countryCode: 'KZ',
      bin: null, address: null, phone: null, email: null,
      defaultCurrencyId: null, accountOwnerUserId: null,
    },
    adminLastName: 'Edge', adminFirstName: 'Admin',
    adminEmail: `edge-${TS}@example.kz`, adminPosition: 'Директор',
  })
  if (orgRes.status !== 200) {
    report.bug({ step: '01', severity: 'critical',
      title: 'Не удалось создать орг', detail: asString(orgRes.data) }); return
  }
  ctx.orgId = orgRes.data.organization.id
  const adminSess = await login(orgRes.data.adminEmail, orgRes.data.adminTempPassword)
  ctx.adminToken = adminSess.accessToken
  check(step, { kind: 'api', description: 'Орг + админ созданы', ok: true,
    detail: `org=${ctx.orgId}` })

  const api = makeClient(ctx.adminToken)

  // 2. Lookups.
  const units = await api.get('/api/catalog/units-of-measure')
  ctx.unitId = units.data?.items?.[0]?.id ?? units.data?.[0]?.id
  const grps = await api.get('/api/catalog/product-groups?pageSize=10')
  ctx.groupId = grps.data?.items?.[0]?.id
  if (!ctx.groupId) {
    const g = await api.post('/api/catalog/product-groups', { name: 'Group', parentId: null })
    ctx.groupId = g.data?.id
  }
  const currencies = await api.get('/api/catalog/currencies?pageSize=200')
  ctx.currencyId = currencies.data?.items?.find((c: { code: string }) => c.code === 'KZT')?.id
  const priceTypes = await api.get('/api/catalog/price-types')
  ctx.retailPriceTypeId = priceTypes.data?.items?.find((p: { isRetail: boolean }) => p.isRetail)?.id

  // 3. Counterparty (поставщик).
  const cp = await api.post('/api/catalog/counterparties', {
    name: 'Edge Supplier', type: 1, bin: '012345678901', phone: '+77001234567',
  })
  ctx.supplierId = cp.data?.id
  check(step, { kind: 'api', description: 'Counterparty создан', ok: cp.status === 201 })

  // 4. Store + RetailPoint из bootstrap.
  const stores = await api.get('/api/catalog/stores?pageSize=10')
  ctx.storeId = stores.data?.items?.[0]?.id
  const rps = await api.get('/api/catalog/retail-points?pageSize=10')
  ctx.retailPointId = rps.data?.items?.[0]?.id

  // 5. Product.
  ctx.productBarcode = generateEan13()
  const prod = await api.post('/api/catalog/products', {
    name: `Edge Product ${TS}`,
    unitOfMeasureId: ctx.unitId, productGroupId: ctx.groupId,
    vat: 12, vatEnabled: true,
    barcodes: [{ code: ctx.productBarcode, type: 1, isPrimary: true }],
    prices: [{ priceTypeId: ctx.retailPriceTypeId, currencyId: ctx.currencyId, amount: 200 }],
  })
  ctx.productId = prod.data?.id
  check(step, { kind: 'api', description: 'Product создан', ok: prod.status === 201,
    detail: prod.status === 201 ? ctx.productId : asString(prod.data) })

  // 6. Supply Draft (10 шт по 100 KZT).
  const supplyRes = await api.post('/api/purchases/supplies', {
    storeId: ctx.storeId, supplierId: ctx.supplierId, currencyId: ctx.currencyId,
    date: new Date().toISOString(),
    lines: [{ productId: ctx.productId, quantity: 10, unitPrice: 100 }],
  })
  ctx.supplyId = supplyRes.data?.id
  check(step, { kind: 'api', description: 'Supply Draft создана', ok: supplyRes.status === 201,
    detail: supplyRes.status === 201 ? ctx.supplyId : asString(supplyRes.data) })
}

export async function step02_post_supply_stock_10({ ctx, step, report }: StepCtx) {
  if (!ctx.adminToken || !ctx.supplyId || !ctx.productId || !ctx.storeId) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken)

  const post = await api.post(`/api/purchases/supplies/${ctx.supplyId}/post`)
  check(step, { kind: 'api', description: 'Supply.Post → 200/204',
    ok: post.status === 200 || post.status === 204, detail: `actual=${post.status}` })

  // Проверяем что stock=10
  const stock = await api.get(`/api/inventory/stock?productId=${ctx.productId}&storeId=${ctx.storeId}`)
  const row = (stock.data?.items ?? []).find((s: { productId: string }) => s.productId === ctx.productId)
  const qty = Number(row?.quantity ?? 0)
  check(step, { kind: 'db', description: 'Stock.Quantity == 10',
    ok: qty === 10, detail: `qty=${qty}` })
  if (qty !== 10) report.bug({ step: '02', severity: 'high',
    title: 'Stock после Supply.Post не равен 10', detail: `получено qty=${qty}` })
}

export async function step03_oversell_blocked({ ctx, step, report }: StepCtx) {
  if (!ctx.adminToken || !ctx.productId || !ctx.storeId || !ctx.retailPointId) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken)

  // Создаём чек с qty=15 (превышает stock=10).
  const sale = await api.post('/api/sales/retail', {
    storeId: ctx.storeId, retailPointId: ctx.retailPointId,
    currencyId: ctx.currencyId,
    date: new Date().toISOString(),
    lines: [{ productId: ctx.productId, quantity: 15, unitPrice: 200, vatPercent: 12 }],
    paidCash: 3000, paidCard: 0,
  })
  check(step, { kind: 'api', description: 'POST RetailSale Draft (qty=15)',
    ok: sale.status === 201, detail: `actual=${sale.status} ${asString(sale.data).slice(0, 120)}` })
  if (sale.status !== 201) return

  const post = await api.post(`/api/sales/retail/${sale.data.id}/post`)
  const ok = post.status === 409
  check(step, { kind: 'api', description: 'POST /post → 409 (oversell)',
    ok, detail: `actual=${post.status} ${asString(post.data).slice(0, 120)}` })
  if (!ok && post.status >= 200 && post.status < 300) {
    report.bug({ step: '03', severity: 'critical',
      title: 'OVERSELL: stock уходит в минус — продажа qty>остаток прошла',
      detail: `Post вернул ${post.status}. Нужен server-side guard: SUM(line.qty) <= stock.quantity per (product,store).` })
  }
}

export async function step04_oversell_stock_unchanged({ ctx, step, report }: StepCtx) {
  if (!ctx.adminToken || !ctx.productId || !ctx.storeId) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken)

  const stock = await api.get(`/api/inventory/stock?productId=${ctx.productId}&storeId=${ctx.storeId}`)
  const row = (stock.data?.items ?? []).find((s: { productId: string }) => s.productId === ctx.productId)
  const qty = Number(row?.quantity ?? 0)
  check(step, { kind: 'db', description: 'Stock остался 10 после заблокированного post',
    ok: qty === 10, detail: `qty=${qty}` })

  // Также: invariant Stock == Σ Movement
  if (ctx.productId && ctx.storeId) {
    const movSum = psql(
      `SELECT COALESCE(SUM("Quantity"),0)::text FROM stock_movements
       WHERE "ProductId"='${ctx.productId}' AND "StoreId"='${ctx.storeId}'`,
    ).trim()
    const sumNum = Number(movSum.split('\n')[0] || '0')
    check(step, { kind: 'db', description: 'Stock == Σ StockMovement (invariant)',
      ok: sumNum === qty, detail: `sum=${sumNum} qty=${qty}` })
  }
}

export async function step05_payment_mismatch_blocked({ ctx, step, report }: StepCtx) {
  if (!ctx.adminToken || !ctx.productId || !ctx.storeId || !ctx.retailPointId) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken)

  const sale = await api.post('/api/sales/retail', {
    storeId: ctx.storeId, retailPointId: ctx.retailPointId, currencyId: ctx.currencyId,
    date: new Date().toISOString(),
    lines: [{ productId: ctx.productId, quantity: 2, unitPrice: 200, vatPercent: 12 }],
    // Total = 400, но оплачено только 300 — должен отвергаться на post.
    paidCash: 100, paidCard: 200,
  })
  if (sale.status !== 201) {
    check(step, { kind: 'api', description: 'Draft создан', ok: false, detail: asString(sale.data) })
    return
  }
  const post = await api.post(`/api/sales/retail/${sale.data.id}/post`)
  const ok = post.status >= 400 && post.status < 500
  check(step, { kind: 'api', description: 'Платёж ≠ Total → 4xx на post',
    ok, detail: `actual=${post.status} ${asString(post.data).slice(0, 120)}` })
  if (!ok) report.bug({ step: '05', severity: 'high',
    title: 'Проведение чека с неверной суммой платежа разрешено',
    detail: `Сумма оплаты 300 при Total=400 — post вернул ${post.status}.` })
}

export async function step06_edit_posted_supply_blocked({ ctx, step, report }: StepCtx) {
  if (!ctx.adminToken || !ctx.supplyId) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken)
  // Supply уже Posted в step02.
  const cur = await api.get(`/api/purchases/supplies/${ctx.supplyId}`)
  if (cur.status !== 200) { step.status = 'skip'; return }
  const put = await api.put(`/api/purchases/supplies/${ctx.supplyId}`, {
    ...cur.data,
    date: cur.data.date,
    lines: (cur.data.lines || []).map((l: { productId: string; quantity: number; unitPrice: number }) =>
      ({ productId: l.productId, quantity: l.quantity, unitPrice: l.unitPrice + 1 })),
  })
  const ok = put.status === 409
  check(step, { kind: 'api', description: 'PUT проведённой Supply → 409',
    ok, detail: `actual=${put.status} ${asString(put.data).slice(0, 100)}` })
  if (!ok && put.status >= 200 && put.status < 300) report.bug({ step: '06', severity: 'high',
    title: 'Posted Supply можно изменить через PUT', detail: asString(put.data) })
}

export async function step07_delete_posted_supply_blocked({ ctx, step, report }: StepCtx) {
  if (!ctx.adminToken || !ctx.supplyId) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken)
  const del = await api.delete(`/api/purchases/supplies/${ctx.supplyId}`)
  const ok = del.status === 409
  check(step, { kind: 'api', description: 'DELETE проведённой Supply → 409',
    ok, detail: `actual=${del.status} ${asString(del.data).slice(0, 100)}` })
  if (!ok && del.status >= 200 && del.status < 300) report.bug({ step: '07', severity: 'high',
    title: 'Posted Supply можно удалить', detail: asString(del.data) })
}

export async function step08_unpost_negative_blocked({ ctx, step, report }: StepCtx) {
  if (!ctx.adminToken || !ctx.productId || !ctx.storeId || !ctx.retailPointId || !ctx.supplyId) {
    step.status = 'skip'; return
  }
  const api = makeClient(ctx.adminToken)

  // Сначала продаём qty=5 (валидно: stock=10 → станет 5).
  const sale = await api.post('/api/sales/retail', {
    storeId: ctx.storeId, retailPointId: ctx.retailPointId, currencyId: ctx.currencyId,
    date: new Date().toISOString(),
    lines: [{ productId: ctx.productId, quantity: 5, unitPrice: 200, vatPercent: 12 }],
    paidCash: 1000, paidCard: 0,
  })
  if (sale.status !== 201) { step.status = 'skip'; step.notes.push(`Draft sale fail: ${asString(sale.data)}`); return }
  const postSale = await api.post(`/api/sales/retail/${sale.data.id}/post`)
  check(step, { kind: 'api', description: 'Sale qty=5 проведён',
    ok: postSale.status === 200 || postSale.status === 204, detail: `actual=${postSale.status}` })
  if (postSale.status >= 400) return

  // Stock теперь 5. Unpost Supply (qty=10) уведёт stock в -5 → должен 409.
  const unpost = await api.post(`/api/purchases/supplies/${ctx.supplyId}/unpost`)
  const ok = unpost.status === 409
  check(step, { kind: 'api', description: 'Unpost Supply при stock<unpost-qty → 409',
    ok, detail: `actual=${unpost.status} ${asString(unpost.data).slice(0, 120)}` })
  if (!ok && unpost.status >= 200 && unpost.status < 300) {
    report.bug({ step: '08', severity: 'critical',
      title: 'Unpost Supply уводит Stock в минус (нет защиты)',
      detail: `После Sale qty=5 stock=5. Unpost Supply qty=10 ⇒ stock=-5. Got HTTP ${unpost.status}.` })
  } else if (unpost.status >= 400 && unpost.status < 500 && unpost.status !== 409) {
    // Сервер тоже отверг, но другим кодом — это semantic gap.
    report.gap(`Unpost Supply при отрицательном остатке вернул ${unpost.status} вместо 409. Ожидался Conflict — единый код для бизнес-конфликтов.`)
  }
}

export async function step09_barcode_unique_within_org({ ctx, step, report }: StepCtx) {
  if (!ctx.adminToken || !ctx.productBarcode || !ctx.unitId || !ctx.groupId) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken)

  const dup = await api.post('/api/catalog/products', {
    name: 'Дубль-штрихкод', unitOfMeasureId: ctx.unitId, productGroupId: ctx.groupId,
    vat: 12, vatEnabled: true,
    barcodes: [{ code: ctx.productBarcode, type: 1, isPrimary: true }],
    prices: [{ priceTypeId: ctx.retailPriceTypeId, currencyId: ctx.currencyId, amount: 300 }],
  })
  const ok = dup.status === 409 || dup.status === 400
  check(step, { kind: 'api', description: 'POST product с тем же barcode → 4xx',
    ok, detail: `actual=${dup.status} ${asString(dup.data).slice(0, 120)}` })
  if (!ok && dup.status >= 200 && dup.status < 300) report.bug({ step: '09', severity: 'high',
    title: 'Дубль штрихкода в одной орге разрешён',
    detail: `POST вернул ${dup.status}. Нарушает уникальный индекс per (OrgId, Barcode).` })
}

export async function step10_barcode_per_tenant({ ctx, step, report }: StepCtx) {
  if (!ctx.productBarcode) { step.status = 'skip'; return }
  // Создаём другую орг и пытаемся использовать тот же штрихкод.
  const sa = await ensureSuperAdmin(ctx)
  const saApi = makeClient(sa)
  const orgRes = await saApi.post('/api/super-admin/organizations', {
    org: {
      name: `Edge Shop2 ${TS}`, countryCode: 'KZ',
      bin: null, address: null, phone: null, email: null,
      defaultCurrencyId: null, accountOwnerUserId: null,
    },
    adminLastName: 'Edge2', adminFirstName: 'Admin',
    adminEmail: `edge2-${TS}@example.kz`, adminPosition: null,
  })
  if (orgRes.status !== 200) { step.status = 'skip'; step.notes.push('Не удалось создать вторую орг'); return }
  const adminSess = await login(orgRes.data.adminEmail, orgRes.data.adminTempPassword)
  const api2 = makeClient(adminSess.accessToken)

  // Lookups для новой орги
  const units = await api2.get('/api/catalog/units-of-measure')
  const u2 = units.data?.items?.[0]?.id ?? units.data?.[0]?.id
  const grps = await api2.get('/api/catalog/product-groups?pageSize=10')
  let g2 = grps.data?.items?.[0]?.id
  if (!g2) {
    const g = await api2.post('/api/catalog/product-groups', { name: 'G2', parentId: null })
    g2 = g.data?.id
  }
  const cur = await api2.get('/api/catalog/currencies?pageSize=200')
  const c2 = cur.data?.items?.find((c: { code: string }) => c.code === 'KZT')?.id
  const pt = await api2.get('/api/catalog/price-types')
  const r2 = pt.data?.items?.find((p: { isRetail: boolean }) => p.isRetail)?.id

  const prod = await api2.post('/api/catalog/products', {
    name: 'Tenant-2 product (same barcode)',
    unitOfMeasureId: u2, productGroupId: g2,
    vat: 12, vatEnabled: true,
    barcodes: [{ code: ctx.productBarcode, type: 1, isPrimary: true }],
    prices: [{ priceTypeId: r2, currencyId: c2, amount: 250 }],
  })
  check(step, { kind: 'api', description: 'POST product с тем же barcode в другой орге → 201',
    ok: prod.status === 201, detail: `actual=${prod.status} ${asString(prod.data).slice(0, 120)}` })
  if (prod.status !== 201) report.bug({ step: '10', severity: 'medium',
    title: 'Per-tenant уникальность штрихкода не работает',
    detail: `Уникальный индекс должен быть на (OrganizationId, Code), сейчас отвергает межтенантное переиспользование. Got ${prod.status}.` })

  // Подсчитаем общее число с этим штрихкодом
  const totalRows = countRows('product_barcodes', `"Code"='${ctx.productBarcode}'`)
  check(step, { kind: 'db', description: 'В product_barcodes 2 записи с этим Code (одна на орг)',
    ok: totalRows === 2, detail: `count=${totalRows}` })
}
