import http from 'node:http'
import type { AddressInfo } from 'node:net'

/**
 * Minimal in-memory Magento 2 REST API for end-to-end tests: enough of /V1 to exercise
 * sync, catalog API and MCP tools, including searchCriteria filtering and paging.
 */

type Item = Record<string, unknown>
const ca = (o: Record<string, unknown>) => Object.entries(o).map(([attribute_code, value]) => ({ attribute_code, value }))

export function fixtures() {
  const products: Item[] = [
    {
      id: 10, sku: 'TEE', name: 'Classic Tee', attribute_set_id: 4, price: 0, status: 1, visibility: 4, type_id: 'configurable',
      created_at: '2026-09-01 10:00:00', updated_at: '2026-10-01 10:00:00',
      extension_attributes: { website_ids: [1], configurable_product_links: [11, 12], category_links: [{ category_id: '3', position: 0 }] },
      product_links: [{ sku: 'TEE', link_type: 'crosssell', linked_product_sku: 'MUG', linked_product_type: 'simple', position: 1 }],
      media_gallery_entries: [{ id: 1, media_type: 'image', label: 'Front', position: 1, disabled: false, types: ['image', 'thumbnail'], file: '/t/e/tee.jpg' }],
      custom_attributes: ca({ brand: 'Acme', description: '<p>Soft cotton tee</p>', url_key: 'classic-tee', color: '5' }),
    },
    {
      id: 11, sku: 'TEE-S', name: 'Classic Tee S', attribute_set_id: 4, price: 20, status: 1, visibility: 1, type_id: 'simple',
      created_at: '2026-09-01 10:00:00', updated_at: '2026-10-01 10:00:01',
      extension_attributes: { website_ids: [1] }, custom_attributes: ca({ brand: 'Acme', color: '5', url_key: 'tee-s' }),
    },
    {
      id: 12, sku: 'TEE-M', name: 'Classic Tee M', attribute_set_id: 4, price: 20, status: 1, visibility: 1, type_id: 'simple',
      created_at: '2026-09-01 10:00:00', updated_at: '2026-10-01 10:00:02',
      extension_attributes: { website_ids: [1] }, custom_attributes: ca({ color: '6', url_key: 'tee-m' }),
    },
    {
      id: 20, sku: 'MUG', name: 'Enamel Mug', attribute_set_id: 4, price: 8, status: 1, visibility: 4, type_id: 'simple',
      created_at: '2026-09-01 10:00:00', updated_at: '2026-10-02 08:00:00',
      extension_attributes: { website_ids: [1] },
      custom_attributes: ca({ brand: 'Acme', special_price: '6.0000', url_key: 'enamel-mug' }),
    },
    {
      id: 30, sku: 'HIDDEN', name: 'Hidden Lamp', attribute_set_id: 4, price: 0, status: 1, visibility: 4, type_id: 'simple',
      created_at: '2026-09-01 10:00:00', updated_at: '2026-10-03 08:00:00',
      extension_attributes: { website_ids: [], category_links: [{ category_id: '4', position: 0 }] },
      custom_attributes: ca({ brand: 'Acme', description: 'A lamp', url_key: 'hidden-lamp' }),
    },
  ]

  return {
    products,
    storeOverrides: { default: { MUG: { name: 'Enamel Mug (UK)' } } } as Record<string, Record<string, Item>>,
    storeViews: [
      { id: 0, code: 'admin', name: 'Admin', website_id: 0, store_group_id: 0, is_active: 1 },
      { id: 1, code: 'default', name: 'UK Store', website_id: 1, store_group_id: 1, is_active: 1 },
    ],
    storeConfigs: [{
      id: 1, code: 'default', website_id: 1, locale: 'en_GB', base_currency_code: 'GBP', default_display_currency_code: 'GBP',
      timezone: 'Europe/London', base_url: 'https://shop.test/', base_media_url: 'https://shop.test/media/',
    }],
    websites: [{ id: 0, code: 'admin', name: 'Admin' }, { id: 1, code: 'base', name: 'Main Website' }],
    attributes: [
      { attribute_id: 93, attribute_code: 'color', frontend_input: 'select', is_required: false, is_user_defined: true, scope: 'global', default_frontend_label: 'Colour', options: [{ label: ' ', value: '' }, { label: 'Red', value: '5' }, { label: 'Blue', value: '6' }] },
      { attribute_id: 140, attribute_code: 'brand', frontend_input: 'text', is_required: true, is_user_defined: true, scope: 'global', default_frontend_label: 'Brand', options: [] },
      { attribute_id: 75, attribute_code: 'description', frontend_input: 'textarea', is_required: false, is_user_defined: false, scope: 'store', default_frontend_label: 'Description', options: [] },
    ],
    attributeSets: [{ attribute_set_id: 4, attribute_set_name: 'Default', entity_type_id: 4 }],
    attributeSetMembers: { 4: [{ attribute_code: 'name', is_required: true }, { attribute_code: 'brand', is_required: true }, { attribute_code: 'color', is_required: false }] } as Record<number, Item[]>,
    categories: [
      { id: 1, parent_id: 0, name: 'Root Catalog', is_active: true, position: 0, level: 0, path: '1', include_in_menu: true },
      { id: 2, parent_id: 1, name: 'Default Category', is_active: true, position: 1, level: 1, path: '1/2', include_in_menu: true },
      { id: 3, parent_id: 2, name: 'T-Shirts', is_active: true, position: 1, level: 2, path: '1/2/3', include_in_menu: true, custom_attributes: ca({ url_key: 't-shirts' }) },
      { id: 4, parent_id: 2, name: 'Archive', is_active: false, position: 2, level: 2, path: '1/2/4', include_in_menu: false },
    ],
    sourceItems: [
      { sku: 'TEE-S', source_code: 'default', quantity: 5, status: 1 },
      { sku: 'TEE-M', source_code: 'default', quantity: 0, status: 0 },
      { sku: 'MUG', source_code: 'default', quantity: 12, status: 1 },
      { sku: 'MUG', source_code: 'leeds', quantity: 3, status: 1 },
      { sku: 'HIDDEN', source_code: 'default', quantity: 0, status: 0 },
    ],
    salable: { 'TEE-S': 5, MUG: 10 } as Record<string, number>,
    orders: [{
      entity_id: 501, increment_id: '000000123', state: 'complete', status: 'complete', created_at: '2026-10-05 12:00:00', updated_at: '2026-10-06 09:00:00',
      store_id: 1, customer_email: 'jane.smith@example.com', customer_firstname: 'Jane', customer_lastname: 'Smith', customer_is_guest: 0,
      grand_total: 16.99, subtotal: 12, shipping_amount: 4.99, discount_amount: 0, tax_amount: 2, total_paid: 16.99, total_refunded: 0,
      order_currency_code: 'GBP', shipping_description: 'Royal Mail - Tracked 48',
      items: [{ item_id: 1, sku: 'MUG', name: 'Enamel Mug', product_type: 'simple', qty_ordered: 2, qty_invoiced: 2, qty_shipped: 2, qty_refunded: 0, qty_canceled: 0, price: 6, row_total_incl_tax: 14.4 }],
      billing_address: { firstname: 'Jane', lastname: 'Smith', street: ['1 High St'], city: 'Leeds', postcode: 'LS1 4AP', country_id: 'GB', telephone: '0113 496 0000', email: 'jane.smith@example.com' },
      payment: { method: 'stripe_payments' },
      status_histories: [{ created_at: '2026-10-06 09:00:00', status: 'complete', comment: 'Shipped', is_customer_notified: 1 }],
      extension_attributes: { shipping_assignments: [{ shipping: { method: 'royalmail_tracked48', address: { firstname: 'Jane', lastname: 'Smith', street: ['1 High St'], city: 'Leeds', postcode: 'LS1 4AP', country_id: 'GB' } } }] },
    }],
    invoices: [{ entity_id: 801, increment_id: '000000123', order_id: 501, state: 2, grand_total: 16.99, created_at: '2026-10-05 12:05:00' }],
    shipments: [{ entity_id: 901, increment_id: '000000077', order_id: 501, created_at: '2026-10-06 09:00:00', tracks: [{ track_number: 'RM123456789GB', title: 'Royal Mail', carrier_code: 'royalmail', created_at: '2026-10-06 09:00:00' }], items: [{ sku: 'MUG', name: 'Enamel Mug', qty: 2 }] }],
  }
}

export type Fixtures = ReturnType<typeof fixtures>

// Sorting a large catalog on every page request is slow; cache per array + field + direction.
const sortCache = new WeakMap<Item[], Map<string, Item[]>>()
function sortedCached(items: Item[], field: string, dir: number): Item[] {
  let byKey = sortCache.get(items)
  if (!byKey) sortCache.set(items, (byKey = new Map()))
  const key = `${field}:${dir}:${items.length}`
  let sorted = byKey.get(key)
  if (!sorted) {
    sorted = [...items].sort((a, b) => (String(a[field]) < String(b[field]) ? -dir : String(a[field]) > String(b[field]) ? dir : 0))
    byKey.set(key, sorted)
  }
  return sorted
}

/** Apply searchCriteria filter groups (AND of ORs), sort and paging like Magento does. */
function search(items: Item[], params: URLSearchParams, fieldAlias: Record<string, string> = {}) {
  const groups = new Map<string, Array<{ field: string; value: string; cond: string }>>()
  for (const [k, v] of params) {
    const m = /^searchCriteria\[filterGroups]\[(\d+)]\[filters]\[(\d+)]\[(field|value|conditionType)]$/.exec(k)
    if (!m) continue
    const key = `${m[1]}`
    const list = groups.get(key) ?? []
    const idx = Number(m[2])
    list[idx] ??= { field: '', value: '', cond: 'eq' }
    if (m[3] === 'field') list[idx]!.field = v
    if (m[3] === 'value') list[idx]!.value = v
    if (m[3] === 'conditionType') list[idx]!.cond = v
    groups.set(key, list)
  }
  const test = (item: Item, f: { field: string; value: string; cond: string }) => {
    const raw = item[fieldAlias[f.field] ?? f.field]
    const v = raw == null ? '' : String(raw)
    switch (f.cond) {
      case 'eq': return v === f.value
      case 'gteq': return v >= f.value
      case 'lteq': return v <= f.value
      case 'in': return f.value.split(',').includes(v)
      case 'like': return v.toLowerCase().includes(f.value.replace(/%/g, '').toLowerCase())
      default: throw new Error(`mock: unsupported condition ${f.cond}`)
    }
  }
  const sortField = params.get('searchCriteria[sortOrders][0][field]')
  const dir = params.get('searchCriteria[sortOrders][0][direction]') === 'DESC' ? -1 : 1
  const source = sortField ? sortedCached(items, sortField, dir) : items
  const out = groups.size ? source.filter(item => [...groups.values()].every(g => g.some(f => test(item, f)))) : source
  const pageSize = Number(params.get('searchCriteria[pageSize]') ?? 20)
  const total = out.length
  const lastPage = Math.max(1, Math.ceil(total / pageSize))
  const page = Math.min(Number(params.get('searchCriteria[currentPage]') ?? 1), lastPage) // Magento repeats the last page
  return { items: out.slice((page - 1) * pageSize, page * pageSize), total_count: total }
}

/** Magento admin accounts for the staff-login flow: username → password (2FA code is always 123456). */
export const ADMIN_USERS: Record<string, string> = { alice: 'Secret123!', bob: 'Hunter2!!' }
export const ADMIN_OTP = '123456'

export async function startMockMagento(data: Fixtures, token = 'magento-test-token') {
  const requests: string[] = []
  const server = http.createServer((req, res) => {
    const url = new URL(req.url!, 'http://mock')
    requests.push(`${req.method} ${url.pathname}`)
    const send = (status: number, body: unknown) => {
      res.writeHead(status, { 'content-type': 'application/json' })
      res.end(JSON.stringify(body))
    }
    // Anonymous admin-token endpoints (staff login check).
    if (req.method === 'POST' && url.pathname.startsWith('/rest/V1/')) {
      let raw = ''
      req.on('data', c => (raw += c))
      req.on('end', () => {
        const b = JSON.parse(raw || '{}') as { username?: string; password?: string; otp?: string }
        // Magento matches admin usernames case-insensitively (MySQL collation).
        const valid = b.username !== undefined && ADMIN_USERS[b.username.toLowerCase()] === b.password
        if (url.pathname === '/rest/V1/integration/admin/token') return send(401, { message: 'Please use the 2fa provider-specific endpoints to obtain a token.' })
        if (url.pathname === '/rest/V1/tfa/provider/google/authenticate') {
          return valid && b.otp === ADMIN_OTP ? send(200, `admin-token-${b.username}`) : send(401, { message: 'The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later.' })
        }
        send(404, { message: 'mock: no POST route' })
      })
      return
    }

    if (req.headers.authorization !== `Bearer ${token}`) return send(401, { message: 'The consumer isn\'t authorized to access %resources.', parameters: { resources: 'Magento_Catalog::products' } })

    const m = /^\/rest\/([^/]+)(\/V1\/.*)$/.exec(url.pathname)
    if (!m) return send(404, { message: 'Request does not match any route.' })
    const [, store, path] = m as unknown as [string, string, string]
    const p = url.searchParams

    if (path === '/V1/store/storeViews') return send(200, data.storeViews)
    if (path === '/V1/store/storeConfigs') return send(200, data.storeConfigs)
    if (path === '/V1/store/websites') return send(200, data.websites)
    if (path === '/V1/products/attributes') return send(200, search(data.attributes, p))
    if (path === '/V1/eav/attribute-sets/list') return send(200, search(data.attributeSets, p))
    const setMembers = /^\/V1\/products\/attribute-sets\/(\d+)\/attributes$/.exec(path)
    if (setMembers) return send(200, data.attributeSetMembers[Number(setMembers[1])] ?? [])
    if (path === '/V1/categories/list') return send(200, search(data.categories, p))

    const scoped = (prod: Item) => ({ ...prod, ...(data.storeOverrides[store]?.[String(prod.sku)] ?? {}) })
    if (path === '/V1/products') return send(200, search(data.products.map(scoped), p, { entity_id: 'id' }))
    const one = /^\/V1\/products\/([^/]+)$/.exec(path)
    if (one) {
      const prod = data.products.find(x => x.sku === decodeURIComponent(one[1]!))
      return prod ? send(200, scoped(prod)) : send(404, { message: 'The product that was requested doesn\'t exist. Verify the product and try again.' })
    }

    if (path === '/V1/inventory/source-items') return send(200, search(data.sourceItems, p))
    const salable = /^\/V1\/inventory\/(get-product-salable-quantity|is-product-salable)\/([^/]+)\/(\d+)$/.exec(path)
    if (salable) {
      const qty = data.salable[decodeURIComponent(salable[2]!)] ?? 0
      return send(200, salable[1] === 'is-product-salable' ? qty > 0 : qty)
    }

    if (path === '/V1/orders') return send(200, search(data.orders, p))
    if (path === '/V1/invoices') return send(200, search(data.invoices, p))
    if (path === '/V1/shipments') return send(200, search(data.shipments, p))

    return send(404, { message: `mock: no route for ${path}` })
  })
  await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
  const { port } = server.address() as AddressInfo
  return { url: `http://127.0.0.1:${port}`, token, requests, close: () => new Promise<void>(r => server.close(() => r())) }
}
