/**
 * Step-handlers для moysklad-import.
 *
 * Поднимаем mock MoySklad (lib/moysklad-mock.ts) на фиксированном порту, на
 * который заранее наведён API через MoySklad:BaseUrl. Прогоняем импорт через
 * реальные эндпоинты /api/admin/moysklad/* + фоновый job /api/admin/jobs/{id}
 * и проверяем результат и в job-счётчиках, и в БД (маппинг полей).
 *
 * Порт mock-сервера ДОЛЖЕН совпадать с MoySklad__BaseUrl, с которым запущен API
 * (см. tests/e2e/README / запуск): http://127.0.0.1:5099/api/remap/1.2/.
 */
import { login, makeClient } from '../lib/api.js'
import { psql } from '../lib/db.js'
import { generateEan13 } from '../lib/barcode.js'
import { startMoySkladMock, type MockHandle, type MsDataset } from '../lib/moysklad-mock.js'
import type { CheckResult, Step, Report } from '../lib/report.js'
import type { AxiosInstance } from 'axios'

const TS = Date.now()
const MOCK_PORT = 5099
const MOCK_BASE = `http://127.0.0.1:${MOCK_PORT}/api/remap/1.2/`

interface Ctx {
  apiOnly: boolean
  superAdminToken?: string
  adminToken?: string
  orgId?: string
  folderId?: string
  ean?: string
  countryName?: string
  countryId?: string
}
interface StepCtx { ctx: Ctx; step: Step; report: Report }

let mock: MockHandle | undefined

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, 200) } catch { return String(x) }
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

function q1(sql: string): string { return (psql(sql).trim().split('\n')[0] ?? '').trim() }
function cpField(orgId: string, name: string, col: string): string {
  return q1(`SELECT "${col}" FROM counterparties WHERE "OrganizationId"='${orgId}' AND "Name"='${name}'`)
}
function cpCount(orgId: string): number {
  return Number(q1(`SELECT count(*) FROM counterparties WHERE "OrganizationId"='${orgId}'`)) || 0
}
function prodCount(orgId: string): number {
  return Number(q1(`SELECT count(*) FROM products WHERE "OrganizationId"='${orgId}'`)) || 0
}

// Строит набор данных mock'а. label/phone варьируем для overwrite-теста.
function buildDataset(ctx: Ctx, phoneSuffix = ''): MsDataset {
  return {
    organization: { name: `Mock Org ${TS}`, inn: '600700800900' },
    counterparties: [
      {
        name: `ТОО Ромашка ${TS}`, legalTitle: `Товарищество Ромашка ${TS}`,
        companyType: 'legal', inn: '123456789012', kpp: 'KPP001',
        phone: `+770111122${phoneSuffix || '33'}`, email: `romashka-${TS}@example.kz`,
        actualAddress: 'Алматы Абая 1', description: 'поставщик овощей', archived: false,
      },
      {
        name: `ИП Иванов ${TS}`, companyType: 'entrepreneur', inn: '850101300123',
        phone: `+770233344${phoneSuffix || '55'}`, email: `ivanov-${TS}@example.kz`,
        legalAddress: 'Астана Победы 5', archived: false,
      },
    ],
    folders: [
      { id: ctx.folderId, name: `Бакалея ${TS}`, pathName: '', archived: false },
    ],
    products: [
      {
        id: `p-${TS}`, name: `Сахар-песок 1кг ${TS}`, article: `SUG-${TS}`, code: `C${TS}`,
        description: 'белый сахар', weighed: false, vat: 12, vatEnabled: true, archived: false,
        trackingType: 'NOT_TRACKED',
        barcodes: [{ ean13: ctx.ean }],
        salePrices: [
          { value: 35000, priceType: { name: 'Цена продажи' } },
          { value: 32000, priceType: { name: 'Розничная цена' } },
        ],
        buyPrice: { value: 25000 },
        productFolder: { meta: { href: `${MOCK_BASE}entity/productfolder/${ctx.folderId}`, type: 'productfolder' } },
        country: { name: ctx.countryName },
      },
    ],
  }
}

async function waitJob(api: AxiosInstance, jobId: string, timeoutMs = 20000): Promise<Record<string, any> | null> {
  const start = Date.now()
  while (Date.now() - start < timeoutMs) {
    const r = await api.get(`/api/admin/jobs/${jobId}`)
    if (r.status === 200 && (r.data.status !== 'Running' || r.data.finishedAt)) return r.data
    await sleep(250)
  }
  return null
}

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

export async function step01_bootstrap_and_connect({ ctx, step, report }: StepCtx) {
  const sa = (await login('admin@food-market.local', 'Admin12345!')).accessToken
  ctx.superAdminToken = sa
  const orgRes = await makeClient(sa).post('/api/super-admin/organizations', {
    org: {
      name: `MoySklad ${TS}`, countryCode: 'KZ',
      bin: null, address: null, phone: null, email: null, defaultCurrencyId: null, accountOwnerUserId: null,
    },
    adminLastName: 'MS', adminFirstName: 'Admin',
    adminEmail: `moysklad-${TS}@example.kz`, adminPosition: null,
  })
  if (orgRes.status !== 200) { report.bug({ step: '01', severity: 'critical', title: 'bootstrap орг', detail: asString(orgRes.data) }); return }
  ctx.orgId = orgRes.data.organization.id
  ctx.adminToken = (await login(orgRes.data.adminEmail, orgRes.data.adminTempPassword)).accessToken
  const api = makeClient(ctx.adminToken)

  // Реальное имя страны из справочника — для детерминированного маппинга country.
  const countries = await api.get('/api/catalog/countries?pageSize=300')
  const kz = (countries.data?.items ?? countries.data ?? []).find((c: { name: string }) => /Казахстан/i.test(c.name))
    ?? (countries.data?.items ?? countries.data ?? [])[0]
  ctx.countryName = kz?.name
  ctx.countryId = kz?.id
  ctx.folderId = `f-${TS}`
  ctx.ean = generateEan13(1)

  // Mock + наведённый на него API.
  mock = await startMoySkladMock(MOCK_PORT)
  mock.setData(buildDataset(ctx))
  check(step, { kind: 'api', description: 'Mock MoySklad поднят', ok: !!mock, detail: MOCK_BASE })

  // Сохранение токена + маскирование.
  const token = `mocktoken${TS}abcd`
  const put = await api.put('/api/admin/moysklad/settings', { token })
  check(step, { kind: 'api', description: 'PUT settings → hasToken + маска',
    ok: put.status === 200 && put.data.hasToken === true && /•/.test(put.data.masked ?? ''),
    detail: `status=${put.status} masked=${put.data?.masked}` })
  const get = await api.get('/api/admin/moysklad/settings')
  check(step, { kind: 'api', description: 'GET settings не отдаёт сырой токен',
    ok: get.data.hasToken === true && !String(get.data.masked ?? '').includes(token),
    detail: `masked=${get.data?.masked}` })

  // test-connection: API сходит в mock entity/organization.
  const test = await api.post('/api/admin/moysklad/test', {})
  check(step, { kind: 'api', description: 'POST test → 200, имя орги из mock',
    ok: test.status === 200 && test.data.organization === `Mock Org ${TS}`,
    detail: `status=${test.status} org=${asString(test.data)}` })
  if (test.status !== 200) report.bug({ step: '01', severity: 'critical',
    title: 'test-connection не дошёл до mock MoySklad',
    detail: `Проверь, что API запущен с MoySklad__BaseUrl=${MOCK_BASE}. Ответ: ${asString(test.data)}` })
}

export async function step02_import_counterparties_create({ ctx, step, report }: StepCtx) {
  if (!ctx.orgId || !mock) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken!)
  const r = await api.post('/api/admin/moysklad/import-counterparties', { overwriteExisting: false })
  check(step, { kind: 'api', description: 'import-counterparties → jobId', ok: r.status === 200 && !!r.data.jobId, detail: `status=${r.status}` })
  if (!r.data?.jobId) return
  const job = await waitJob(api, r.data.jobId)
  check(step, { kind: 'api', description: 'job Succeeded', ok: job?.status === 'Succeeded', detail: `status=${job?.status} msg=${job?.message}` })
  check(step, { kind: 'api', description: 'Created=2, Skipped=0',
    ok: job?.created === 2 && job?.skipped === 0, detail: `created=${job?.created} updated=${job?.updated} skipped=${job?.skipped}` })

  check(step, { kind: 'db', description: 'В БД 2 контрагента', ok: cpCount(ctx.orgId) === 2, detail: `count=${cpCount(ctx.orgId)}` })
  // Маппинг ТОО Ромашка (companyType=legal → LegalEntity=1).
  const ro = `ТОО Ромашка ${TS}`
  check(step, { kind: 'db', description: 'Ромашка: Type=1(LegalEntity), Bin=inn, TaxNumber=kpp',
    ok: cpField(ctx.orgId, ro, 'Type') === '1' && cpField(ctx.orgId, ro, 'Bin') === '123456789012' && cpField(ctx.orgId, ro, 'TaxNumber') === 'KPP001',
    detail: `Type=${cpField(ctx.orgId, ro, 'Type')} Bin=${cpField(ctx.orgId, ro, 'Bin')} Tax=${cpField(ctx.orgId, ro, 'TaxNumber')}` })
  check(step, { kind: 'db', description: 'Ромашка: LegalName=legalTitle, Address=actualAddress, Notes=description',
    ok: cpField(ctx.orgId, ro, 'LegalName') === `Товарищество Ромашка ${TS}` && cpField(ctx.orgId, ro, 'Address') === 'Алматы Абая 1' && cpField(ctx.orgId, ro, 'Notes') === 'поставщик овощей',
    detail: `Legal=${cpField(ctx.orgId, ro, 'LegalName')} Addr=${cpField(ctx.orgId, ro, 'Address')}` })
  // Маппинг ИП Иванов (entrepreneur → Individual=2, Address=legalAddress fallback).
  const iv = `ИП Иванов ${TS}`
  check(step, { kind: 'db', description: 'Иванов: Type=2(Individual), Address=legalAddress (fallback)',
    ok: cpField(ctx.orgId, iv, 'Type') === '2' && cpField(ctx.orgId, iv, 'Address') === 'Астана Победы 5',
    detail: `Type=${cpField(ctx.orgId, iv, 'Type')} Addr=${cpField(ctx.orgId, iv, 'Address')}` })
  if (cpCount(ctx.orgId) !== 2) report.bug({ step: '02', severity: 'high', title: 'Импортировано не 2 контрагента', detail: `count=${cpCount(ctx.orgId)}` })
}

export async function step03_import_counterparties_idempotent({ ctx, step, report }: StepCtx) {
  if (!ctx.orgId || !mock) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken!)
  const r = await api.post('/api/admin/moysklad/import-counterparties', { overwriteExisting: false })
  const job = await waitJob(api, r.data.jobId)
  check(step, { kind: 'api', description: 'job Succeeded', ok: job?.status === 'Succeeded', detail: `status=${job?.status}` })
  check(step, { kind: 'api', description: 'Skipped=2, Created=0 (идемпотентность по имени)',
    ok: job?.skipped === 2 && job?.created === 0, detail: `created=${job?.created} skipped=${job?.skipped}` })
  check(step, { kind: 'db', description: 'Дублей нет — в БД по-прежнему 2', ok: cpCount(ctx.orgId) === 2, detail: `count=${cpCount(ctx.orgId)}` })
  if (cpCount(ctx.orgId) !== 2) report.bug({ step: '03', severity: 'critical', title: 'Повторный импорт породил дубли контрагентов', detail: `count=${cpCount(ctx.orgId)}` })
}

export async function step04_import_counterparties_overwrite({ ctx, step, report }: StepCtx) {
  if (!ctx.orgId || !mock) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken!)
  // Меняем телефоны в источнике и импортируем с overwrite=true.
  mock.setData(buildDataset(ctx, '99'))
  const r = await api.post('/api/admin/moysklad/import-counterparties', { overwriteExisting: true })
  const job = await waitJob(api, r.data.jobId)
  check(step, { kind: 'api', description: 'job Succeeded', ok: job?.status === 'Succeeded', detail: `status=${job?.status}` })
  check(step, { kind: 'api', description: 'Updated=2, Created=0',
    ok: job?.updated === 2 && job?.created === 0, detail: `created=${job?.created} updated=${job?.updated} skipped=${job?.skipped}` })
  check(step, { kind: 'db', description: 'Телефон Ромашки обновлён на новый',
    ok: cpField(ctx.orgId, `ТОО Ромашка ${TS}`, 'Phone') === '+77011112299',
    detail: `phone=${cpField(ctx.orgId, `ТОО Ромашка ${TS}`, 'Phone')}` })
  check(step, { kind: 'db', description: 'Кол-во не выросло (обновление, не вставка)', ok: cpCount(ctx.orgId) === 2, detail: `count=${cpCount(ctx.orgId)}` })
  // Возвращаем исходные данные, чтобы дальнейшие шаги были предсказуемы.
  mock.setData(buildDataset(ctx))
}

export async function step05_import_products_create({ ctx, step, report }: StepCtx) {
  if (!ctx.orgId || !mock) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken!)
  const r = await api.post('/api/admin/moysklad/import-products', { overwriteExisting: false })
  check(step, { kind: 'api', description: 'import-products → jobId', ok: r.status === 200 && !!r.data.jobId, detail: `status=${r.status}` })
  if (!r.data?.jobId) return
  const job = await waitJob(api, r.data.jobId)
  check(step, { kind: 'api', description: 'job Succeeded', ok: job?.status === 'Succeeded', detail: `status=${job?.status} msg=${job?.message} errors=${asString(job?.errors)}` })
  check(step, { kind: 'api', description: 'Created=1, GroupsCreated>=1',
    ok: job?.created === 1 && (job?.groupsCreated ?? 0) >= 1, detail: `created=${job?.created} groups=${job?.groupsCreated}` })

  const art = `SUG-${TS}`
  const pid = q1(`SELECT "Id" FROM products WHERE "OrganizationId"='${ctx.orgId}' AND "Article"='${art}'`)
  check(step, { kind: 'db', description: 'Товар создан с артикулом из MoySklad', ok: !!pid, detail: `id=${pid}` })
  if (!pid) { report.bug({ step: '05', severity: 'high', title: 'Товар не импортирован', detail: `errors=${asString(job?.errors)}` }); return }

  const vat = q1(`SELECT "Vat" FROM products WHERE "Id"='${pid}'`)
  const pack = q1(`SELECT "Packaging" FROM products WHERE "Id"='${pid}'`)
  const marked = q1(`SELECT "IsMarked" FROM products WHERE "Id"='${pid}'`)
  const ref = q1(`SELECT "ReferencePrice" FROM products WHERE "Id"='${pid}'`)
  check(step, { kind: 'db', description: 'Vat=12, Packaging=1(Piece, weighed=false), IsMarked=false',
    ok: Number(vat) === 12 && pack === '1' && (marked === 'f' || marked === 'false'), detail: `vat=${vat} pack=${pack} marked=${marked}` })
  check(step, { kind: 'db', description: 'ReferencePrice = buyPrice/100 = 250', ok: Number(ref) === 250, detail: `ref=${ref}` })

  // Группа из productFolder.
  const grpName = q1(`SELECT g."Name" FROM products p JOIN product_groups g ON g."Id"=p."ProductGroupId" WHERE p."Id"='${pid}'`)
  check(step, { kind: 'db', description: 'Группа = productFolder «Бакалея»', ok: grpName === `Бакалея ${TS}`, detail: `group=${grpName}` })
  // Страна по имени.
  const country = q1(`SELECT "CountryOfOriginId" FROM products WHERE "Id"='${pid}'`)
  check(step, { kind: 'db', description: 'CountryOfOrigin сопоставлена по имени',
    ok: !!country && country === ctx.countryId, detail: `country=${country} expected=${ctx.countryId}` })
  // Розничная цена = salePrice «Розничная» / 100 = 320.
  const price = q1(`SELECT "Amount" FROM product_prices WHERE "ProductId"='${pid}'`)
  check(step, { kind: 'db', description: 'Розничная цена = 320 (salePrice «Розничная»/100)', ok: Number(price) === 320, detail: `amount=${price}` })
  // Штрихкод.
  const bc = q1(`SELECT "Code" FROM product_barcodes WHERE "ProductId"='${pid}'`)
  check(step, { kind: 'db', description: 'Штрихкод ean13 импортирован', ok: bc === ctx.ean, detail: `bc=${bc} expected=${ctx.ean}` })
}

export async function step06_import_products_idempotent({ ctx, step, report }: StepCtx) {
  if (!ctx.orgId || !mock) { step.status = 'skip'; return }
  const api = makeClient(ctx.adminToken!)
  const before = prodCount(ctx.orgId)
  const r = await api.post('/api/admin/moysklad/import-products', { overwriteExisting: false })
  const job = await waitJob(api, r.data.jobId)
  check(step, { kind: 'api', description: 'job Succeeded', ok: job?.status === 'Succeeded', detail: `status=${job?.status}` })
  check(step, { kind: 'api', description: 'Skipped=1, Created=0 (идемпотентность по артикулу)',
    ok: job?.skipped === 1 && job?.created === 0, detail: `created=${job?.created} skipped=${job?.skipped}` })
  check(step, { kind: 'db', description: 'Кол-во товаров не изменилось (дублей нет)',
    ok: prodCount(ctx.orgId) === before, detail: `before=${before} after=${prodCount(ctx.orgId)}` })
  if (prodCount(ctx.orgId) !== before) report.bug({ step: '06', severity: 'critical', title: 'Повторный импорт породил дубль товара', detail: `before=${before} after=${prodCount(ctx.orgId)}` })
}

export async function step07_cleanup({ step }: StepCtx) {
  if (mock) { await mock.close(); mock = undefined }
  check(step, { kind: 'api', description: 'Mock MoySklad остановлен', ok: true })
}
