/**
 * Stage catalog: products/groups/counterparties CRUD + multi-tenant
 * изоляция. Создаём 2 org-A/B через /api/auth/signup, под каждым
 * admin'ом проверяем что объекты не пересекаются.
 */
import { login, makeClient } from '../lib/api.js'
import type { CheckResult, Step, Report } from '../lib/report.js'
import { generateEan13 } from '../lib/barcode.js'

type Org = {
  orgId: string
  email: string
  password: string
  token: string
  productGroupRootId?: string  // системная «Все товары»
  priceTypeRetailId?: string
  currencyKztId?: string
  unitKgId?: string
}
type Ctx = {
  apiOnly: boolean
  ts: number
  orgA?: Org
  orgB?: Org
  groupChildIdA?: string
  productIdA?: string
  counterpartyIdA?: string
}

interface StepCtx { ctx: Ctx; step: Step; report: Report }

const TS = Date.now()

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
  return JSON.stringify(x)
}

async function signupAndLogin(suffix: string): Promise<Org> {
  const api = makeClient()
  const email = `stage-cat-${suffix}-${TS}@food-market.local`
  const password = 'StageCat12345!'
  const orgName = `Cat Org ${suffix} ${TS}`
  const r = await api.post('/api/auth/signup', {
    email, password, organizationName: orgName,
    phone: suffix === 'a' ? '+77011111111' : '+77022222222', plan: 'start',
  })
  if (r.status !== 200) throw new Error(`signup ${suffix} failed: ${r.status} ${JSON.stringify(r.data)}`)
  const sess = await login(email, password)
  return {
    orgId: r.data.organizationId,
    email, password,
    token: sess.accessToken,
  }
}

async function fetchCommonRefs(org: Org): Promise<void> {
  const api = makeClient(org.token)
  const [groups, prices, units, currencies] = await Promise.all([
    api.get('/api/catalog/product-groups'),
    api.get('/api/catalog/price-types'),
    api.get('/api/catalog/units-of-measure'),
    api.get('/api/catalog/currencies'),
  ])
  org.productGroupRootId = groups.data?.items?.find((g: { name: string; parentId: string | null }) => g.parentId == null)?.id
  org.priceTypeRetailId = prices.data?.items?.find((p: { isRequired: boolean }) => p.isRequired)?.id
  org.currencyKztId = currencies.data?.items?.find((c: { code: string }) => c.code === 'KZT')?.id
  org.unitKgId = units.data?.items?.find((u: { code: string }) => u.code === '166')?.id
}

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

export async function cat01_setup_two_orgs({ ctx, step, report }: StepCtx) {
  ctx.ts = TS
  ctx.orgA = await signupAndLogin('a')
  await new Promise(r => setTimeout(r, 1000))  // не словить rate-limit на signup
  ctx.orgB = await signupAndLogin('b')
  await fetchCommonRefs(ctx.orgA)
  await fetchCommonRefs(ctx.orgB)

  check(step, {
    kind: 'api',
    description: 'Org A создана, refs загружены',
    ok: !!ctx.orgA.productGroupRootId && !!ctx.orgA.priceTypeRetailId && !!ctx.orgA.currencyKztId && !!ctx.orgA.unitKgId,
    detail: `org=${ctx.orgA.orgId.slice(0,8)} group=${!!ctx.orgA.productGroupRootId} price=${!!ctx.orgA.priceTypeRetailId} cur=${!!ctx.orgA.currencyKztId} unit=${!!ctx.orgA.unitKgId}`,
  })
  check(step, {
    kind: 'api',
    description: 'Org B создана, refs загружены',
    ok: !!ctx.orgB.productGroupRootId && !!ctx.orgB.priceTypeRetailId && !!ctx.orgB.currencyKztId && !!ctx.orgB.unitKgId,
    detail: `org=${ctx.orgB.orgId.slice(0,8)}`,
  })
  check(step, {
    kind: 'api',
    description: 'Org A и Org B — разные organizationId',
    ok: ctx.orgA.orgId !== ctx.orgB.orgId,
    detail: `${ctx.orgA.orgId} vs ${ctx.orgB.orgId}`,
  })
  // Группы должны быть РАЗНЫЕ — у каждой org свой корневой «Все товары».
  check(step, {
    kind: 'api',
    description: 'Корневая группа «Все товары» у org A и B — разные записи',
    ok: ctx.orgA.productGroupRootId !== ctx.orgB.productGroupRootId,
    detail: `A.root=${ctx.orgA.productGroupRootId} B.root=${ctx.orgB.productGroupRootId}`,
  })
}

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

export async function cat02_product_group_crud({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA) { step.status = 'skip'; return }
  const api = makeClient(ctx.orgA.token)

  // Создаём корневую под Org A
  const created = await api.post('/api/catalog/product-groups', {
    name: `Группа ${TS}`, parentId: null, sortOrder: 10, markupPercent: 30,
  })
  check(step, {
    kind: 'api',
    description: 'POST product-groups (root) → 201',
    ok: created.status === 201,
    detail: `${created.status} ${asString(created.data).slice(0, 200)}`,
  })
  if (created.status !== 201) {
    report.bug({ step: 'cat02', severity: 'high', title: 'Не создаётся product-group root', detail: asString(created.data) })
    return
  }
  const rootId = created.data.id
  check(step, {
    kind: 'api',
    description: 'OrganizationId назначен автоматически (мультитенантность)',
    ok: created.data.organizationId === ctx.orgA.orgId,
    detail: `${created.data.organizationId} vs ${ctx.orgA.orgId}`,
  })

  // Дочерняя
  const child = await api.post('/api/catalog/product-groups', {
    name: 'Подгруппа A', parentId: rootId, sortOrder: 0, markupPercent: 50,
  })
  check(step, {
    kind: 'api',
    description: 'POST product-groups (child) → 201, path "Группа .../Подгруппа A"',
    ok: child.status === 201 && /\//.test(child.data?.path ?? ''),
    detail: `${child.status} path="${child.data?.path}"`,
  })
  ctx.groupChildIdA = child.data?.id

  // Обновляем дочернюю
  const upd = await api.put(`/api/catalog/product-groups/${ctx.groupChildIdA}`, {
    name: 'Подгруппа A (rename)', parentId: rootId, sortOrder: 1, markupPercent: 55,
  })
  check(step, {
    kind: 'api',
    description: 'PUT product-groups child → 204',
    ok: upd.status === 204,
    detail: `${upd.status} ${asString(upd.data).slice(0, 200)}`,
  })

  // Удалить group с подгруппами — должно отказаться
  const delWithChild = await api.delete(`/api/catalog/product-groups/${rootId}`)
  check(step, {
    kind: 'api',
    description: 'DELETE root c подгруппой → 400 "Нельзя удалить группу с подгруппами"',
    ok: delWithChild.status === 400 && /подгруп/i.test(asString(delWithChild.data)),
    detail: `${delWithChild.status} ${asString(delWithChild.data).slice(0, 200)}`,
  })

  // Удалить дочернюю — должно сработать
  const delChild = await api.delete(`/api/catalog/product-groups/${ctx.groupChildIdA}`)
  check(step, {
    kind: 'api',
    description: 'DELETE child (без товаров) → 204',
    ok: delChild.status === 204,
    detail: `${delChild.status}`,
  })

  // Cleanup: удалить и root тоже (она нам не нужна для cat06; cat06 использует фабричную «Все товары» + новый product)
  const delRoot = await api.delete(`/api/catalog/product-groups/${rootId}`)
  check(step, {
    kind: 'api',
    description: 'Cleanup DELETE root (теперь без детей) → 204',
    ok: delRoot.status === 204,
    detail: `${delRoot.status}`,
  })
}

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

export async function cat03_product_crud({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA) { step.status = 'skip'; return }
  const api = makeClient(ctx.orgA.token)
  const a = ctx.orgA

  const barcode1 = generateEan13(1)
  const barcode2 = generateEan13(2)
  const article1 = `ART-${TS}-A1`

  // Create
  const created = await api.post('/api/catalog/products', {
    name: `Product ${TS} A`,
    article: article1,
    description: 'тестовый',
    unitOfMeasureId: a.unitKgId,
    vat: 0, vatEnabled: false,
    productGroupId: a.productGroupRootId,
    barcodes: [{ code: barcode1, type: 1, isPrimary: true }],
    prices: [{ priceTypeId: a.priceTypeRetailId, amount: 1000, currencyId: a.currencyKztId }],
  })
  check(step, {
    kind: 'api',
    description: 'POST product → 201',
    ok: created.status === 201,
    detail: `${created.status} ${asString(created.data).slice(0, 250)}`,
  })
  if (created.status !== 201) return
  ctx.productIdA = created.data.id

  // Get
  const got = await api.get(`/api/catalog/products/${ctx.productIdA}`)
  check(step, {
    kind: 'api',
    description: 'GET product by id → 200',
    ok: got.status === 200 && got.data?.article === article1,
    detail: `${got.status} article=${got.data?.article}`,
  })

  // Update — меняем имя, добавляем второй штрихкод
  const upd = await api.put(`/api/catalog/products/${ctx.productIdA}`, {
    name: `Product ${TS} A (upd)`,
    article: article1,
    description: 'обновлённый',
    unitOfMeasureId: a.unitKgId,
    vat: 0, vatEnabled: false,
    productGroupId: a.productGroupRootId,
    barcodes: [
      { code: barcode1, type: 1, isPrimary: true },
      { code: barcode2, type: 1, isPrimary: false },
    ],
    prices: [{ priceTypeId: a.priceTypeRetailId, amount: 1100, currencyId: a.currencyKztId }],
  })
  check(step, {
    kind: 'api',
    description: 'PUT product → 204',
    ok: upd.status === 204,
    detail: `${upd.status} ${asString(upd.data).slice(0, 200)}`,
  })

  // Дубликат штрихкода — создание второго товара с тем же barcode1 → 400
  const dup = await api.post('/api/catalog/products', {
    name: `Dup ${TS}`,
    article: `ART-${TS}-A2`,
    unitOfMeasureId: a.unitKgId,
    vat: 0, vatEnabled: false,
    productGroupId: a.productGroupRootId,
    barcodes: [{ code: barcode1, type: 1, isPrimary: true }],
    prices: [{ priceTypeId: a.priceTypeRetailId, amount: 500, currencyId: a.currencyKztId }],
  })
  check(step, {
    kind: 'api',
    description: 'POST с дубликатом штрихкода → 400 "уже используется"',
    ok: dup.status === 400 && /используется|already/i.test(asString(dup.data)),
    detail: `${dup.status} ${asString(dup.data).slice(0, 200)}`,
  })

  // Дубликат артикула — другая штрихкод но тот же article
  const dupArt = await api.post('/api/catalog/products', {
    name: `DupArt ${TS}`,
    article: article1,
    unitOfMeasureId: a.unitKgId,
    vat: 0, vatEnabled: false,
    productGroupId: a.productGroupRootId,
    barcodes: [{ code: generateEan13(3), type: 1, isPrimary: true }],
    prices: [{ priceTypeId: a.priceTypeRetailId, amount: 500, currencyId: a.currencyKztId }],
  })
  check(step, {
    kind: 'api',
    description: 'POST с дубликатом article → 400 "Артикул уже занят"',
    ok: dupArt.status === 400 && /артикул|занят/i.test(asString(dupArt.data)),
    detail: `${dupArt.status} ${asString(dupArt.data).slice(0, 200)}`,
  })

  // Поиск
  const list = await api.get('/api/catalog/products?search=' + encodeURIComponent(`Product ${TS}`))
  check(step, {
    kind: 'api',
    description: 'GET products?search возвращает наш product',
    ok: list.status === 200 && list.data?.items?.some((p: { id: string }) => p.id === ctx.productIdA),
    detail: `${list.status} total=${list.data?.total}`,
  })
}

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

export async function cat04_counterparty_crud({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA) { step.status = 'skip'; return }
  const api = makeClient(ctx.orgA.token)

  // Юрлицо (CounterpartyType.LegalEntity=1)
  const legal = await api.post('/api/catalog/counterparties', {
    name: `ТОО Тест ${TS}`, legalName: `ТОО Тест-Полное ${TS}`, type: 1,
    bin: '999888777666', iin: null, taxNumber: null,
    countryId: null, address: 'Алматы', phone: '+77011112233', email: 'test@example.kz',
    bankName: 'Halyk', bankAccount: 'KZ123', bik: 'HSBKKZKX', contactPerson: 'Иван', notes: null,
  })
  check(step, {
    kind: 'api',
    description: 'POST counterparty (Legal) → 201',
    ok: legal.status === 201,
    detail: `${legal.status} ${asString(legal.data).slice(0, 200)}`,
  })
  ctx.counterpartyIdA = legal.data?.id

  // Физлицо (CounterpartyType.Individual=2)
  const indiv = await api.post('/api/catalog/counterparties', {
    name: `Иванов И.И. ${TS}`, legalName: null, type: 2,
    bin: null, iin: '880101300123', taxNumber: null,
    countryId: null, address: null, phone: '+77002223344', email: null,
    bankName: null, bankAccount: null, bik: null, contactPerson: null, notes: null,
  })
  check(step, {
    kind: 'api',
    description: 'POST counterparty (Individual) → 201',
    ok: indiv.status === 201,
    detail: `${indiv.status} ${asString(indiv.data).slice(0, 200)}`,
  })

  // Невалидный телефон
  const badPhone = await api.post('/api/catalog/counterparties', {
    name: `Bad ${TS}`, type: 1, phone: 'not-a-phone',
    bin: null, iin: null, taxNumber: null, countryId: null,
    legalName: null, address: null, email: null, bankName: null, bankAccount: null,
    bik: null, contactPerson: null, notes: null,
  })
  check(step, {
    kind: 'api',
    description: 'POST counterparty с кривым телефоном → 400',
    ok: badPhone.status === 400 && /кор?рект|казах/i.test(asString(badPhone.data)),
    detail: `${badPhone.status} ${asString(badPhone.data).slice(0, 200)}`,
  })

  // Update
  if (ctx.counterpartyIdA) {
    const upd = await api.put(`/api/catalog/counterparties/${ctx.counterpartyIdA}`, {
      name: `ТОО Тест UPD ${TS}`, legalName: null, type: 1,
      bin: '999888777666', iin: null, taxNumber: null,
      countryId: null, address: null, phone: '+77011112233', email: null,
      bankName: null, bankAccount: null, bik: null, contactPerson: null, notes: 'updated',
    })
    check(step, {
      kind: 'api',
      description: 'PUT counterparty → 204',
      ok: upd.status === 204,
      detail: `${upd.status} ${asString(upd.data).slice(0, 200)}`,
    })
  }
}

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

export async function cat05_multi_tenant_isolation({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA || !ctx.orgB || !ctx.productIdA || !ctx.counterpartyIdA) {
    step.status = 'skip'; step.notes.push('Нужны orgA/B и созданные сущности из cat03/cat04')
    return
  }
  const apiB = makeClient(ctx.orgB.token)

  // List
  const productsB = await apiB.get('/api/catalog/products?pageSize=200')
  const seenA = productsB.data?.items?.some((p: { id: string }) => p.id === ctx.productIdA)
  check(step, {
    kind: 'api',
    description: 'Org B GET /products НЕ содержит товар org A',
    ok: !seenA,
    detail: `org B total=${productsB.data?.total}, seenA=${seenA}`,
  })
  if (seenA) {
    report.bug({
      step: 'cat05', severity: 'critical',
      title: 'P0 multi-tenant утечка: Org B видит товар Org A в /api/catalog/products',
      detail: `Tenant query filter не применён или обходится. productId=${ctx.productIdA}`,
    })
  }

  const cpsB = await apiB.get('/api/catalog/counterparties?pageSize=200')
  const seenCpA = cpsB.data?.items?.some((c: { id: string }) => c.id === ctx.counterpartyIdA)
  check(step, {
    kind: 'api',
    description: 'Org B GET /counterparties НЕ содержит контрагента org A',
    ok: !seenCpA,
    detail: `org B total=${cpsB.data?.total}, seenA=${seenCpA}`,
  })
  if (seenCpA) {
    report.bug({
      step: 'cat05', severity: 'critical',
      title: 'P0 multi-tenant утечка: Org B видит контрагента Org A в /api/catalog/counterparties',
      detail: `counterpartyId=${ctx.counterpartyIdA}`,
    })
  }

  // GET by id
  const productByIdB = await apiB.get(`/api/catalog/products/${ctx.productIdA}`)
  check(step, {
    kind: 'api',
    description: 'Org B GET /products/{id-чужой} → 404',
    ok: productByIdB.status === 404,
    detail: `${productByIdB.status}`,
  })
  if (productByIdB.status !== 404) {
    report.bug({
      step: 'cat05', severity: 'critical',
      title: 'P0: Org B читает product Org A по id',
      detail: `status=${productByIdB.status}, body=${asString(productByIdB.data).slice(0, 200)}`,
    })
  }

  const cpByIdB = await apiB.get(`/api/catalog/counterparties/${ctx.counterpartyIdA}`)
  check(step, {
    kind: 'api',
    description: 'Org B GET /counterparties/{id-чужой} → 404',
    ok: cpByIdB.status === 404,
    detail: `${cpByIdB.status}`,
  })

  // UPDATE/DELETE
  const updByB = await apiB.put(`/api/catalog/products/${ctx.productIdA}`, {
    name: 'hijack', article: 'x',
    unitOfMeasureId: ctx.orgB.unitKgId, vat: 0, vatEnabled: false,
    productGroupId: ctx.orgB.productGroupRootId,
    barcodes: [{ code: generateEan13(4), type: 1, isPrimary: true }],
    prices: [{ priceTypeId: ctx.orgB.priceTypeRetailId, amount: 1, currencyId: ctx.orgB.currencyKztId }],
  })
  check(step, {
    kind: 'api',
    description: 'Org B PUT /products/{id-чужой} → 404 (нельзя угнать)',
    ok: updByB.status === 404,
    detail: `${updByB.status} ${asString(updByB.data).slice(0, 200)}`,
  })
  if (updByB.status !== 404) {
    report.bug({
      step: 'cat05', severity: 'critical',
      title: 'P0: Org B может PUT товара Org A',
      detail: `status=${updByB.status}`,
    })
  }

  const delByB = await apiB.delete(`/api/catalog/products/${ctx.productIdA}`)
  check(step, {
    kind: 'api',
    description: 'Org B DELETE /products/{id-чужой} → 404',
    ok: delByB.status === 404,
    detail: `${delByB.status}`,
  })
}

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

export async function cat06_fk_protection_groups({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA || !ctx.productIdA) { step.status = 'skip'; return }
  const api = makeClient(ctx.orgA.token)

  // Bootstrap-группа «Все товары» создана с IsSystem=true. Контроллер
  // проверяет IsSystem ПЕРВОЙ — поэтому даже с привязанным товаром мы
  // получим «Системную группу удалить нельзя» (а не сообщение про товары).
  // Это OK как защита: системную в принципе нельзя удалить никогда.
  const delRoot = await api.delete(`/api/catalog/product-groups/${ctx.orgA.productGroupRootId}`)
  check(step, {
    kind: 'api',
    description: 'DELETE системной «Все товары» → 400 "Системную группу"',
    ok: delRoot.status === 400 && /систем/i.test(asString(delRoot.data)),
    detail: `${delRoot.status} ${asString(delRoot.data).slice(0, 200)}`,
  })

  // Проверка реального FK-чека на не-системной группе: создадим обычную
  // группу, привяжем к ней нашего товара через PUT, попробуем удалить группу.
  const grp = await api.post('/api/catalog/product-groups', {
    name: `FK Test ${TS}`, parentId: null, sortOrder: 0, markupPercent: null,
  })
  if (grp.status !== 201) {
    step.notes.push(`grp.create: ${grp.status} ${asString(grp.data).slice(0, 120)}`)
    return
  }
  const grpId = grp.data.id

  // Привяжем product к этой группе через PUT
  const updProd = await api.put(`/api/catalog/products/${ctx.productIdA}`, {
    name: 'rebind', article: `A-${TS}`,
    unitOfMeasureId: ctx.orgA.unitKgId, vat: 0, vatEnabled: false,
    productGroupId: grpId,
    barcodes: [{ code: '2099811100015', type: 1, isPrimary: true }],
    prices: [{ priceTypeId: ctx.orgA.priceTypeRetailId, amount: 100, currencyId: ctx.orgA.currencyKztId }],
  })
  check(step, {
    kind: 'api',
    description: 'PUT product → перевешиваем в новую группу',
    ok: updProd.status === 204,
    detail: `${updProd.status}`,
  })

  const delGrp = await api.delete(`/api/catalog/product-groups/${grpId}`)
  check(step, {
    kind: 'api',
    description: 'DELETE не-системной группы с привязанным товаром → 400 "товарами"',
    ok: delGrp.status === 400 && /товар/i.test(asString(delGrp.data)),
    detail: `${delGrp.status} ${asString(delGrp.data).slice(0, 200)}`,
  })

  // Cleanup: удалим product → теперь группа удалится.
  const delProduct = await api.delete(`/api/catalog/products/${ctx.productIdA}`)
  check(step, {
    kind: 'api',
    description: 'DELETE product → 204',
    ok: delProduct.status === 204,
    detail: `${delProduct.status}`,
  })

  const delGrpEmpty = await api.delete(`/api/catalog/product-groups/${grpId}`)
  check(step, {
    kind: 'api',
    description: 'DELETE пустой группы (после удаления товара) → 204',
    ok: delGrpEmpty.status === 204,
    detail: `${delGrpEmpty.status}`,
  })
}
