/**
 * Stage Enter: CRUD + post + unpost + edge + multi-tenant. Создаём
 * 2 org через signup, под каждой делаем по продукту, затем проверяем
 * Enter-pipeline и изоляцию.
 */
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
  storeId: string
  productId: string
  productName: string
  currencyId: string
  unitKgId: string
  groupId: string
  priceTypeRetailId: string
}
type Ctx = {
  apiOnly: boolean
  ts: number
  orgA?: Org
  orgB?: Org
  enterIdA?: string
  enterNumberA?: string
  qty1?: number
  qty2?: number
  cost1?: number
  cost2?: number
}
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 bootstrapOrg(suffix: string, phone: string, barcodeIdx: number): Promise<Org> {
  const api = makeClient()
  const email = `stage-enter-${suffix}-${TS}@food-market.local`
  const password = 'StageEnt12345!'
  const orgName = `Enter Org ${suffix} ${TS}`
  const r = await api.post('/api/auth/signup', { email, password, organizationName: orgName, phone, plan: 'start' })
  if (r.status !== 200) throw new Error(`signup ${suffix} failed: ${r.status} ${JSON.stringify(r.data)}`)
  const sess = await login(email, password)
  const auth = makeClient(sess.accessToken)
  const [stores, units, groups, prices, currencies] = await Promise.all([
    auth.get('/api/catalog/stores'),
    auth.get('/api/catalog/units-of-measure'),
    auth.get('/api/catalog/product-groups'),
    auth.get('/api/catalog/price-types'),
    auth.get('/api/catalog/currencies'),
  ])
  const storeId = stores.data.items.find((s: { isMain: boolean }) => s.isMain).id
  const unitKgId = units.data.items.find((u: { code: string }) => u.code === '166').id
  const groupId = groups.data.items.find((g: { parentId: string | null }) => g.parentId == null).id
  const priceTypeRetailId = prices.data.items.find((p: { isRequired: boolean }) => p.isRequired).id
  const currencyId = currencies.data.items.find((c: { code: string }) => c.code === 'KZT').id

  const prod = await auth.post('/api/catalog/products', {
    name: `Enter Prod ${suffix} ${TS}`,
    article: `A-${suffix}-${TS}`,
    unitOfMeasureId: unitKgId, vat: 0, vatEnabled: false,
    productGroupId: groupId,
    barcodes: [{ code: generateEan13(barcodeIdx), type: 1, isPrimary: true }],
    prices: [{ priceTypeId: priceTypeRetailId, amount: 100, currencyId }],
  })
  if (prod.status !== 201) throw new Error(`product ${suffix} create failed: ${prod.status} ${JSON.stringify(prod.data)}`)

  return {
    orgId: r.data.organizationId,
    email, password, token: sess.accessToken,
    storeId, productId: prod.data.id, productName: prod.data.name,
    currencyId, unitKgId, groupId, priceTypeRetailId,
  }
}

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

export async function ent01_setup({ ctx, step, report }: StepCtx) {
  ctx.ts = TS
  ctx.orgA = await bootstrapOrg('a', '+77011110001', 11)
  await new Promise(r => setTimeout(r, 900))
  ctx.orgB = await bootstrapOrg('b', '+77011110002', 12)
  ctx.qty1 = 10
  ctx.qty2 = 5
  ctx.cost1 = 200
  ctx.cost2 = 350
  check(step, { kind: 'api', description: 'Org A с product+store готов', ok: !!ctx.orgA.productId, detail: `${ctx.orgA.orgId}` })
  check(step, { kind: 'api', description: 'Org B с product+store готов', ok: !!ctx.orgB.productId, detail: `${ctx.orgB.orgId}` })
}

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

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

  const product2 = await api.post('/api/catalog/products', {
    name: `Enter Prod A2 ${TS}`,
    article: `A2-${TS}`,
    unitOfMeasureId: a.unitKgId, vat: 0, vatEnabled: false,
    productGroupId: a.groupId,
    barcodes: [{ code: generateEan13(13), type: 1, isPrimary: true }],
    prices: [{ priceTypeId: a.priceTypeRetailId, amount: 500, currencyId: a.currencyId }],
  })
  const product2Id = product2.data.id
  ;(ctx as Ctx & { product2Id?: string }).product2Id = product2Id

  const res = await api.post('/api/inventory/enters', {
    date: new Date().toISOString(),
    storeId: a.storeId,
    currencyId: a.currencyId,
    notes: 'тестовый енторинг',
    lines: [
      { productId: a.productId, quantity: ctx.qty1, unitCost: ctx.cost1 },
      { productId: product2Id, quantity: ctx.qty2, unitCost: ctx.cost2 },
    ],
  })
  check(step, {
    kind: 'api',
    description: 'POST /api/inventory/enters → 201',
    ok: res.status === 201,
    detail: `${res.status} ${asString(res.data).slice(0, 200)}`,
  })
  if (res.status !== 201) return
  ctx.enterIdA = res.data.id
  ctx.enterNumberA = res.data.number
  const expectedTotal = ctx.qty1! * ctx.cost1! + ctx.qty2! * ctx.cost2!
  check(step, {
    kind: 'api',
    description: `Total = sum(qty*cost) = ${expectedTotal}`,
    ok: Math.abs((res.data.total ?? 0) - expectedTotal) < 0.001,
    detail: `total=${res.data.total} vs ${expectedTotal}`,
  })
  check(step, {
    kind: 'api',
    description: 'Номер сгенерирован формата О-YYYY-NNNNNN',
    ok: /^О-\d{4}-\d{6}$/.test(res.data.number ?? ''),
    detail: `number=${res.data.number}`,
  })
  check(step, {
    kind: 'api',
    description: 'Lines.Count = 2',
    ok: (res.data.lines?.length ?? 0) === 2,
    detail: `lines=${res.data.lines?.length}`,
  })
  check(step, {
    kind: 'api',
    description: 'Status = Draft (0) или строка "Draft"',
    ok: res.data.status === 0 || res.data.status === 'Draft',
    detail: `status=${res.data.status}`,
  })
}

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

export async function ent03_update_draft({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA || !ctx.enterIdA) { step.status = 'skip'; return }
  const a = ctx.orgA
  const api = makeClient(a.token)
  const product2Id = (ctx as Ctx & { product2Id?: string }).product2Id!

  // Меняем 1 строку: только product, qty=20, без 2-й
  const upd = await api.put(`/api/inventory/enters/${ctx.enterIdA}`, {
    date: new Date().toISOString(),
    storeId: a.storeId,
    currencyId: a.currencyId,
    notes: 'обновлено',
    lines: [
      { productId: a.productId, quantity: 20, unitCost: 150 },
    ],
  })
  check(step, {
    kind: 'api',
    description: 'PUT enter Draft → 204',
    ok: upd.status === 204,
    detail: `${upd.status} ${asString(upd.data).slice(0, 200)}`,
  })
  if (upd.status !== 204) {
    report.bug({
      step: 'ent03', severity: 'high',
      title: 'PUT enter не работает — возможно EF8 nav-collection bug',
      detail: `EntersController.Update использует RemoveRange + Clear + nav.Add, тот же паттерн что был в ProductsController. После фикса в pt 2 этот контроллер тоже надо переводить на ExecuteDelete+DbSet.Add. body=${asString(upd.data).slice(0, 200)}`,
    })
    return
  }
  // Проверим что total пересчитан
  const got = await api.get(`/api/inventory/enters/${ctx.enterIdA}`)
  check(step, {
    kind: 'api',
    description: 'GET после PUT: Total = 20*150 = 3000, Lines.Count = 1',
    ok: got.data?.total === 3000 && got.data?.lines?.length === 1,
    detail: `total=${got.data?.total} lines=${got.data?.lines?.length}`,
  })
  // Возвращаем 2-строчное состояние для post-теста
  const restore = await api.put(`/api/inventory/enters/${ctx.enterIdA}`, {
    date: new Date().toISOString(),
    storeId: a.storeId,
    currencyId: a.currencyId,
    notes: 'restored',
    lines: [
      { productId: a.productId, quantity: ctx.qty1, unitCost: ctx.cost1 },
      { productId: product2Id, quantity: ctx.qty2, unitCost: ctx.cost2 },
    ],
  })
  check(step, {
    kind: 'api',
    description: 'PUT restore с 2 строками для post-теста → 204',
    ok: restore.status === 204,
    detail: `${restore.status}`,
  })
}

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

export async function ent04_post({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA || !ctx.enterIdA) { step.status = 'skip'; return }
  const a = ctx.orgA
  const api = makeClient(a.token)
  const product2Id = (ctx as Ctx & { product2Id?: string }).product2Id!

  // Запомним stock до проведения
  const stockBefore = await api.get(`/api/inventory/stock?storeId=${a.storeId}`)
  const beforeMap = new Map<string, number>()
  for (const row of (stockBefore.data?.items ?? [])) beforeMap.set(row.productId, row.quantity ?? 0)

  const post = await api.post(`/api/inventory/enters/${ctx.enterIdA}/post`, {})
  check(step, {
    kind: 'api',
    description: 'POST {id}/post → 204',
    ok: post.status === 204,
    detail: `${post.status} ${asString(post.data).slice(0, 200)}`,
  })

  // Stock после
  const stockAfter = await api.get(`/api/inventory/stock?storeId=${a.storeId}`)
  const afterMap = new Map<string, number>()
  for (const row of (stockAfter.data?.items ?? [])) afterMap.set(row.productId, row.quantity ?? 0)

  const delta1 = (afterMap.get(a.productId) ?? 0) - (beforeMap.get(a.productId) ?? 0)
  const delta2 = (afterMap.get(product2Id) ?? 0) - (beforeMap.get(product2Id) ?? 0)
  check(step, {
    kind: 'api',
    description: `Stock.Quantity продукта 1 += ${ctx.qty1}`,
    ok: Math.abs(delta1 - ctx.qty1!) < 0.001,
    detail: `delta=${delta1}`,
  })
  check(step, {
    kind: 'api',
    description: `Stock.Quantity продукта 2 += ${ctx.qty2}`,
    ok: Math.abs(delta2 - ctx.qty2!) < 0.001,
    detail: `delta=${delta2}`,
  })

  // StockMovement[type=Enter] на оба продукта
  const movs = await api.get(`/api/inventory/movements?storeId=${a.storeId}&take=200`)
  const myMovs = (movs.data?.items ?? []).filter((m: { documentId: string }) => m.documentId === ctx.enterIdA)
  check(step, {
    kind: 'api',
    description: `Создано минимум 2 StockMovement с DocumentId = enterIdA`,
    ok: myMovs.length >= 2,
    detail: `movs=${myMovs.length}, types=${myMovs.map((m: { type: number | string }) => m.type).join(',')}`,
  })

  // Статус документа = Posted
  const got = await api.get(`/api/inventory/enters/${ctx.enterIdA}`)
  check(step, {
    kind: 'api',
    description: 'После Post: Status = Posted (1), PostedAt не null',
    ok: (got.data?.status === 1 || got.data?.status === 'Posted') && got.data?.postedAt != null,
    detail: `status=${got.data?.status} postedAt=${got.data?.postedAt}`,
  })
}

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

export async function ent05_post_idempotent_or_conflict({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA || !ctx.enterIdA) { step.status = 'skip'; return }
  const api = makeClient(ctx.orgA.token)
  const post2 = await api.post(`/api/inventory/enters/${ctx.enterIdA}/post`, {})
  check(step, {
    kind: 'api',
    description: 'Повторный POST {id}/post → 409 «Документ уже проведён»',
    ok: post2.status === 409 && /уже провед/i.test(asString(post2.data)),
    detail: `${post2.status} ${asString(post2.data).slice(0, 200)}`,
  })
}

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

export async function ent06_unpost({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA || !ctx.enterIdA) { step.status = 'skip'; return }
  const a = ctx.orgA
  const api = makeClient(a.token)
  const product2Id = (ctx as Ctx & { product2Id?: string }).product2Id!

  const stockBefore = await api.get(`/api/inventory/stock?storeId=${a.storeId}`)
  const beforeMap = new Map<string, number>()
  for (const row of (stockBefore.data?.items ?? [])) beforeMap.set(row.productId, row.quantity ?? 0)

  const unpost = await api.post(`/api/inventory/enters/${ctx.enterIdA}/unpost`, {})
  check(step, {
    kind: 'api',
    description: 'POST {id}/unpost → 204',
    ok: unpost.status === 204,
    detail: `${unpost.status} ${asString(unpost.data).slice(0, 200)}`,
  })

  const stockAfter = await api.get(`/api/inventory/stock?storeId=${a.storeId}`)
  const afterMap = new Map<string, number>()
  for (const row of (stockAfter.data?.items ?? [])) afterMap.set(row.productId, row.quantity ?? 0)
  const delta1 = (afterMap.get(a.productId) ?? 0) - (beforeMap.get(a.productId) ?? 0)
  const delta2 = (afterMap.get(product2Id) ?? 0) - (beforeMap.get(product2Id) ?? 0)
  check(step, {
    kind: 'api',
    description: `После Unpost: stock продукта 1 -= ${ctx.qty1}`,
    ok: Math.abs(delta1 + ctx.qty1!) < 0.001,
    detail: `delta=${delta1}`,
  })
  check(step, {
    kind: 'api',
    description: `После Unpost: stock продукта 2 -= ${ctx.qty2}`,
    ok: Math.abs(delta2 + ctx.qty2!) < 0.001,
    detail: `delta=${delta2}`,
  })

  const got = await api.get(`/api/inventory/enters/${ctx.enterIdA}`)
  check(step, {
    kind: 'api',
    description: 'После Unpost: Status = Draft (0), PostedAt = null',
    ok: (got.data?.status === 0 || got.data?.status === 'Draft') && got.data?.postedAt == null,
    detail: `status=${got.data?.status} postedAt=${got.data?.postedAt}`,
  })
}

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

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

  // Re-post enter
  await api.post(`/api/inventory/enters/${ctx.enterIdA}/post`, {})

  // Сделаем «расход» этого товара через ещё один пост enter с минус-вьюхой
  // нельзя — Enter не принимает отрицательные qty. Делаем Loss (списание)
  // продукта 1 на весь введённый qty, чтобы stock стал = 0 → unpost должен
  // упасть 409.
  // LossReason — enum, не FK. Defect=0.
  const loss = await api.post('/api/inventory/losses', {
    date: new Date().toISOString(),
    storeId: a.storeId, currencyId: a.currencyId,
    reason: 0,
    notes: 'forced consumption for unpost guard test',
    lines: [{ productId: a.productId, quantity: ctx.qty1, unitCost: 1 }],
  })
  if (loss.status !== 201) {
    step.notes.push(`Loss.create unexpected: ${loss.status} ${asString(loss.data).slice(0, 200)}`)
    return
  }
  const lossPost = await api.post(`/api/inventory/losses/${loss.data.id}/post`, {})
  if (lossPost.status !== 204) {
    step.notes.push(`Loss.post unexpected: ${lossPost.status} ${asString(lossPost.data).slice(0, 200)}`)
    return
  }

  // Теперь stock первого продукта = 0. Try unpost
  const unpost = await api.post(`/api/inventory/enters/${ctx.enterIdA}/unpost`, {})
  check(step, {
    kind: 'api',
    description: 'Unpost после расхода → 409 с conflicts (остаток уйдёт в минус)',
    ok: unpost.status === 409 && /минус|расход/i.test(asString(unpost.data)),
    detail: `${unpost.status} ${asString(unpost.data).slice(0, 200)}`,
  })

  // Cleanup: unpost loss → unpost enter
  await api.post(`/api/inventory/losses/${loss.data.id}/unpost`, {}).catch(() => {})
}

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

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

  // Empty lines
  const emptyLines = await api.post('/api/inventory/enters', {
    date: new Date().toISOString(), storeId: a.storeId, currencyId: a.currencyId,
    lines: [],
  })
  check(step, {
    kind: 'api',
    description: 'POST с пустыми lines → 400',
    ok: emptyLines.status === 400,
    detail: `${emptyLines.status} ${asString(emptyLines.data).slice(0, 200)}`,
  })

  // No storeId
  const noStore = await api.post('/api/inventory/enters', {
    date: new Date().toISOString(), storeId: '00000000-0000-0000-0000-000000000000',
    currencyId: a.currencyId,
    lines: [{ productId: a.productId, quantity: 1, unitCost: 1 }],
  })
  check(step, {
    kind: 'api',
    description: 'POST с Guid.Empty storeId → 400',
    ok: noStore.status === 400,
    detail: `${noStore.status} ${asString(noStore.data).slice(0, 200)}`,
  })

  // Negative qty (Range(0, 1e10) — отрицательные блокирует валидатор)
  const neg = await api.post('/api/inventory/enters', {
    date: new Date().toISOString(), storeId: a.storeId, currencyId: a.currencyId,
    lines: [{ productId: a.productId, quantity: -5, unitCost: 100 }],
  })
  check(step, {
    kind: 'api',
    description: 'POST с qty<0 → 400 (FluentValidation Range)',
    ok: neg.status === 400,
    detail: `${neg.status} ${asString(neg.data).slice(0, 200)}`,
  })
}

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

export async function ent09_multi_tenant_isolation({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA || !ctx.orgB || !ctx.enterIdA) { step.status = 'skip'; return }
  const apiB = makeClient(ctx.orgB.token)

  const list = await apiB.get('/api/inventory/enters?pageSize=200')
  const sees = (list.data?.items ?? []).some((e: { id: string }) => e.id === ctx.enterIdA)
  check(step, {
    kind: 'api',
    description: 'Org B GET /enters НЕ содержит enter org A',
    ok: !sees,
    detail: `total=${list.data?.total} sees=${sees}`,
  })
  if (sees) {
    report.bug({
      step: 'ent09', severity: 'critical',
      title: 'P0 multi-tenant утечка: enter Org A виден из org B',
      detail: `enterId=${ctx.enterIdA}`,
    })
  }

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

  const post = await apiB.post(`/api/inventory/enters/${ctx.enterIdA}/post`, {})
  check(step, {
    kind: 'api',
    description: 'Org B POST /enters/{id-чужой}/post → 404',
    ok: post.status === 404,
    detail: `${post.status}`,
  })
}

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

export async function ent10_concurrent_put_no_version({ ctx, step, report }: StepCtx) {
  if (!ctx.orgA || !ctx.enterIdA) { step.status = 'skip'; return }
  const a = ctx.orgA
  const api = makeClient(a.token)
  const product2Id = (ctx as Ctx & { product2Id?: string }).product2Id!

  // Enter ранее проведён в ent07, потом по идее unpost не прошёл (409). Если документ всё ещё Posted —
  // unpost. Затем дублирующий PUT в гонке.
  const got = await api.get(`/api/inventory/enters/${ctx.enterIdA}`)
  if (got.data?.status === 1 || got.data?.status === 'Posted') {
    // Сначала восстановим stock: создадим enter +qty1, проведём — тогда unpost сработает.
    const tmp = await api.post('/api/inventory/enters', {
      date: new Date().toISOString(), storeId: a.storeId, currencyId: a.currencyId,
      notes: 'restock for concurrent test',
      lines: [{ productId: a.productId, quantity: ctx.qty1, unitCost: 1 }],
    })
    if (tmp.status === 201) {
      await api.post(`/api/inventory/enters/${tmp.data.id}/post`, {})
    }
    // Теперь try unpost оригинал
    await api.post(`/api/inventory/enters/${ctx.enterIdA}/unpost`, {})
  }

  // Параллельный PUT — два запроса одновременно с разными notes
  const body = (notes: string) => ({
    date: new Date().toISOString(),
    storeId: a.storeId, currencyId: a.currencyId, notes,
    lines: [
      { productId: a.productId, quantity: ctx.qty1, unitCost: ctx.cost1 },
      { productId: product2Id, quantity: ctx.qty2, unitCost: ctx.cost2 },
    ],
  })
  const [r1, r2] = await Promise.all([
    api.put(`/api/inventory/enters/${ctx.enterIdA}`, body('parallel-1')),
    api.put(`/api/inventory/enters/${ctx.enterIdA}`, body('parallel-2')),
  ])
  const statuses = [r1.status, r2.status].sort((a, b) => a - b)
  // Enter — НЕ IVersionedEntity. Поэтому оба PUT'a проходят (204+204) с
  // last-write-wins семантикой. Если в будущем Enter получит IVersionedEntity,
  // ожидаем [204, 409]. Сейчас фиксируем фактическое поведение и помечаем
  // gap, если оба прошли.
  check(step, {
    kind: 'api',
    description: 'Поведение под двумя параллельными PUT (without RowVersion) — last-write-wins',
    ok: statuses[0] === 204 && (statuses[1] === 204 || statuses[1] === 409),
    detail: `statuses=[${statuses.join(', ')}]`,
  })
  if (statuses[0] === 204 && statuses[1] === 204) {
    report.gap(
      'Enter не реализует IVersionedEntity → нет xmin concurrency-токена. ' +
      'При параллельных PUT обоих в Draft выигрывает последний (lost-update). ' +
      'TD-6 добавил xmin на Supply/Demand/RetailSale/Transfer/InventoryDoc — ' +
      'Enter/Loss/SupplierReturn/CustomerReturn остались без защиты. Это logic gap, ' +
      'желательно добавить, как минимум вызывается из UI редактора.',
    )
  }
}
