import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import http from 'node:http';
import pg from 'pg';
import { spawn } from 'node:child_process';

const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:postgres@127.0.0.1:54322/postgres';
const tenantId = '00000000-0000-0000-0000-000000000001';
const studentUserId = '00000000-0000-0000-0000-000000000101';
const planId = '00000000-0000-0000-0000-000000000201';
const regionId = '00000000-0000-0000-0000-000000000301';

const ids = {
  paymentOrder: '10000000-0000-0000-0000-000000000701',
  payment: '10000000-0000-0000-0000-000000000702',
  refundOrder: '10000000-0000-0000-0000-000000000703',
  refundPayment: '10000000-0000-0000-0000-000000000704',
  refundRequest: '10000000-0000-0000-0000-000000000705',
  refundEntitlement: '10000000-0000-0000-0000-000000000706',
  providerBillJobWechat: '10000000-0000-0000-0000-000000000707',
  providerBillJobAlipay: '10000000-0000-0000-0000-000000000708',
  providerBillOrderWechat: '10000000-0000-0000-0000-000000000709',
  providerBillPaymentWechat: '10000000-0000-0000-0000-000000000710',
  providerBillOrderAlipay: '10000000-0000-0000-0000-000000000711',
  providerBillPaymentAlipay: '10000000-0000-0000-0000-000000000712',
};

const orderNos = {
  payment: 'CW-WX-PAY-001',
  refund: 'CW-WX-REFUND-001',
  providerBillWechat: 'CW-WX-BILL-001',
  providerBillAlipay: 'CW-ALI-BILL-001',
};
const refundNo = 'RF-CW-WX-001';
const providerBillDate = '2026-06-29';

const paymentFixture = (() => {
  const wechatMerchant = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
  const alipayApp = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
  return {
    wechatMerchantPrivateKey: wechatMerchant.privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
    alipayAppPrivateKey: alipayApp.privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
    wechatApiV3Key: '12345678901234567890123456789012',
  };
})();

function getFreePort() {
  return new Promise((resolve, reject) => {
    const server = http.createServer();
    server.listen(0, '127.0.0.1', () => {
      const address = server.address();
      server.close(() => resolve(address.port));
    });
    server.on('error', reject);
  });
}

async function startFakeWechatPayServer() {
  const port = await getFreePort();
  const baseUrl = `http://127.0.0.1:${port}`;
  const requests = [];
  const billRows = [
    {
      transactionType: 'payment',
      orderNo: orderNos.providerBillWechat,
      providerTradeNo: `wx-bill-trade-${orderNos.providerBillWechat}`,
      amountCents: 990,
      providerStatus: 'SUCCESS',
      paidAt: '2026-06-29T10:00:00+08:00',
    },
    {
      transactionType: 'payment',
      orderNo: 'CW-WX-PROVIDER-ONLY',
      providerTradeNo: 'wx-provider-only-trade',
      amountCents: 1888,
      providerStatus: 'SUCCESS',
    },
  ];
  const billBody = Buffer.from(JSON.stringify({ rows: billRows }), 'utf8');
  const billHash = crypto.createHash('sha256').update(billBody).digest('hex');
  const server = http.createServer((req, res) => {
    const url = new URL(req.url || '/', baseUrl);
    let raw = '';
    req.on('data', chunk => {
      raw += chunk.toString();
    });
    req.on('end', () => {
      requests.push({ method: req.method, pathname: url.pathname, search: url.search, body: raw ? JSON.parse(raw) : {} });

      if (req.method === 'GET' && url.pathname.startsWith('/v3/pay/transactions/out-trade-no/')) {
        const outTradeNo = decodeURIComponent(url.pathname.split('/').pop() || '');
        res.writeHead(200, { 'content-type': 'application/json' });
        res.end(JSON.stringify({
          appid: 'wx-worker-appid',
          mchid: 'wx-worker-mchid',
          out_trade_no: outTradeNo,
          transaction_id: `wx-trade-${outTradeNo}`,
          trade_state: 'SUCCESS',
          success_time: '2026-06-29T08:00:00+08:00',
          amount: { total: outTradeNo === orderNos.refund ? 500 : 990, currency: 'CNY' },
        }));
        return;
      }

      if (req.method === 'GET' && url.pathname.startsWith('/v3/refund/domestic/refunds/')) {
        const outRefundNo = decodeURIComponent(url.pathname.split('/').pop() || '');
        res.writeHead(200, { 'content-type': 'application/json' });
        res.end(JSON.stringify({
          refund_id: `refund-${outRefundNo}`,
          out_refund_no: outRefundNo,
          status: 'SUCCESS',
          amount: { refund: 500, total: 500, currency: 'CNY' },
          success_time: '2026-06-29T09:00:00+08:00',
        }));
        return;
      }

      if (req.method === 'GET' && url.pathname === '/v3/bill/tradebill') {
        res.writeHead(200, { 'content-type': 'application/json' });
        res.end(JSON.stringify({
          hash_type: 'SHA256',
          hash_value: billHash,
          download_url: `${baseUrl}/download/wechat-tradebill.json`,
        }));
        return;
      }

      if (req.method === 'GET' && url.pathname === '/download/wechat-tradebill.json') {
        res.writeHead(200, { 'content-type': 'application/json' });
        res.end(billBody);
        return;
      }

      res.writeHead(404, { 'content-type': 'application/json' });
      res.end(JSON.stringify({ code: 'NOT_FOUND' }));
    });
  });
  await new Promise((resolve, reject) => {
    server.once('error', reject);
    server.listen(port, '127.0.0.1', resolve);
  });
  return {
    paymentQueryEndpoint: `${baseUrl}/v3/pay/transactions/out-trade-no`,
    refundEndpoint: `${baseUrl}/v3/refund/domestic/refunds`,
    tradeBillEndpoint: `${baseUrl}/v3/bill/tradebill`,
    requests,
    close: () => new Promise(resolve => server.close(resolve)),
  };
}

async function startFakeAlipayServer() {
  const port = await getFreePort();
  const baseUrl = `http://127.0.0.1:${port}`;
  const requests = [];
  const billRows = [
    {
      transactionType: 'payment',
      orderNo: orderNos.providerBillAlipay,
      providerTradeNo: `ali-bill-trade-${orderNos.providerBillAlipay}`,
      amountCents: 1288,
      providerStatus: 'TRADE_SUCCESS',
      paidAt: '2026-06-29 11:00:00',
    },
  ];
  const billBody = Buffer.from(JSON.stringify({ rows: billRows }), 'utf8');
  const server = http.createServer((req, res) => {
    const url = new URL(req.url || '/', baseUrl);
    let raw = '';
    req.on('data', chunk => {
      raw += chunk.toString();
    });
    req.on('end', () => {
      requests.push({ method: req.method, pathname: url.pathname, search: url.search, body: raw });
      if (req.method === 'POST' && url.pathname === '/gateway.do') {
        res.writeHead(200, { 'content-type': 'application/json' });
        res.end(JSON.stringify({
          alipay_data_dataservice_bill_downloadurl_query_response: {
            code: '10000',
            msg: 'Success',
            bill_download_url: `${baseUrl}/download/alipay-tradebill.json`,
          },
        }));
        return;
      }
      if (req.method === 'GET' && url.pathname === '/download/alipay-tradebill.json') {
        res.writeHead(200, { 'content-type': 'application/json' });
        res.end(billBody);
        return;
      }
      res.writeHead(404, { 'content-type': 'application/json' });
      res.end(JSON.stringify({ code: 'NOT_FOUND' }));
    });
  });
  await new Promise((resolve, reject) => {
    server.once('error', reject);
    server.listen(port, '127.0.0.1', resolve);
  });
  return {
    endpoint: `${baseUrl}/gateway.do`,
    requests,
    close: () => new Promise(resolve => server.close(resolve)),
  };
}

async function runWorkerOnce() {
  const child = spawn(process.execPath, ['apps/worker/dist/apps/worker/src/index.js', '--once', '--job', 'commerce'], {
    cwd: process.cwd(),
    env: {
      ...process.env,
      DATABASE_URL: databaseUrl,
      WORKER_COMMERCE_BATCH_SIZE: '20',
      WORKER_COMMERCE_MIN_AGE_SECONDS: '0',
      WORKER_COMMERCE_REQUEST_TIMEOUT_MS: '5000',
    },
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true,
  });
  let output = '';
  child.stdout.on('data', chunk => {
    output += chunk.toString();
  });
  child.stderr.on('data', chunk => {
    output += chunk.toString();
  });
  const code = await new Promise(resolve => child.on('exit', resolve));
  assert.equal(code, 0, `worker should exit 0\n${output}`);
  assert.match(output, /commerce batch processed=\d+/, 'worker output should include commerce summary');
  assert.ok(!output.includes(paymentFixture.wechatApiV3Key), 'worker output must not leak WeChat API v3 key');
  assert.ok(!output.includes('PRIVATE KEY'), 'worker output must not leak merchant private key');
  return output;
}

async function runProviderBillsWorkerOnce() {
  const child = spawn(process.execPath, ['apps/worker/dist/apps/worker/src/index.js', '--once', '--job', 'provider-bills'], {
    cwd: process.cwd(),
    env: {
      ...process.env,
      DATABASE_URL: databaseUrl,
      WORKER_PROVIDER_BILL_BATCH_SIZE: '10',
      WORKER_PROVIDER_BILL_ID: 'provider-bills-integration-test',
      WORKER_COMMERCE_REQUEST_TIMEOUT_MS: '5000',
    },
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true,
  });
  let output = '';
  child.stdout.on('data', chunk => {
    output += chunk.toString();
  });
  child.stderr.on('data', chunk => {
    output += chunk.toString();
  });
  const code = await new Promise(resolve => child.on('exit', resolve));
  assert.equal(code, 0, `provider bill worker should exit 0\n${output}`);
  assert.match(output, /provider-bills batch processed=\d+/, 'worker output should include provider bill summary');
  assert.ok(!output.includes(paymentFixture.wechatApiV3Key), 'provider bill worker output must not leak WeChat API v3 key');
  assert.ok(!output.includes('PRIVATE KEY'), 'provider bill worker output must not leak private keys');
  return output;
}

async function cleanup(pool) {
  await pool.query(
    `
    delete from public.commerce_bill_download_jobs
    where tenant_id = $1
      and (
        id in ($2::uuid, $3::uuid)
        or source_name like 'provider-bill:%:2026-06-29:%'
      )
    `,
    [tenantId, ids.providerBillJobWechat, ids.providerBillJobAlipay],
  );
  await pool.query(
    `
    delete from public.commerce_reconciliation_batches
    where tenant_id = $1
      and source = 'provider_download'
      and source_name like 'provider-bill:%:2026-06-29:%'
    `,
    [tenantId],
  );
  await pool.query(
    `
    delete from public.payment_events
    where tenant_id = $1
      and (
        event_id like $2
        or event_id like $3
      )
    `,
    [tenantId, `${tenantId}:payment-query:CW-WX-%`, `${tenantId}:refund-query:RF-CW-%`],
  );
  await pool.query(
    `
    delete from public.commerce_refund_events
    where tenant_id = $1 and refund_request_id = $2
    `,
    [tenantId, ids.refundRequest],
  );
  await pool.query(
    `
    delete from public.commerce_refund_requests
    where tenant_id = $1 and id = $2
    `,
    [tenantId, ids.refundRequest],
  );
  await pool.query(
    `
    delete from public.entitlements
    where tenant_id = $1
      and (
        id = $2
        or source_id in ($3::uuid, $4::uuid)
      )
    `,
    [tenantId, ids.refundEntitlement, ids.paymentOrder, ids.refundOrder],
  );
  await pool.query(
    `
    delete from public.payments
    where tenant_id = $1 and id in ($2::uuid, $3::uuid, $4::uuid, $5::uuid)
    `,
    [tenantId, ids.payment, ids.refundPayment, ids.providerBillPaymentWechat, ids.providerBillPaymentAlipay],
  );
  await pool.query(
    `
    delete from public.orders
    where tenant_id = $1 and id in ($2::uuid, $3::uuid, $4::uuid, $5::uuid)
    `,
    [tenantId, ids.paymentOrder, ids.refundOrder, ids.providerBillOrderWechat, ids.providerBillOrderAlipay],
  );
}

async function seed(pool, fakeWechat, fakeAlipay) {
  await pool.query(
    `
    insert into app_private.tenant_secrets (
      tenant_id, secret_scope, secret_key, secret_json, provider, last_rotated_at
    )
    values ($1, 'payment', 'wechat_pay', $2::jsonb, 'wechat_pay', now())
    on conflict (tenant_id, secret_scope, secret_key)
    do update set secret_json = excluded.secret_json,
                  provider = excluded.provider,
                  last_rotated_at = now(),
                  updated_at = now()
    `,
    [tenantId, JSON.stringify({
      privateKey: paymentFixture.wechatMerchantPrivateKey,
      apiV3Key: paymentFixture.wechatApiV3Key,
    })],
  );

  await pool.query(
    `
    insert into app_private.tenant_secrets (
      tenant_id, secret_scope, secret_key, secret_json, provider, last_rotated_at
    )
    values ($1, 'payment', 'alipay', $2::jsonb, 'alipay', now())
    on conflict (tenant_id, secret_scope, secret_key)
    do update set secret_json = excluded.secret_json,
                  provider = excluded.provider,
                  last_rotated_at = now(),
                  updated_at = now()
    `,
    [tenantId, JSON.stringify({
      privateKey: paymentFixture.alipayAppPrivateKey,
    })],
  );

  await pool.query(
    `
    insert into public.tenant_payment_accounts (
      tenant_id, provider, mode, display_name, status, config_public
    )
    values ($1, 'wechat_pay', 'tenant_collect', 'Worker WeChat Pay', 'active', $2::jsonb)
    on conflict (tenant_id, provider)
    do update set mode = excluded.mode,
                  display_name = excluded.display_name,
                  status = excluded.status,
                  config_public = excluded.config_public,
                  updated_at = now()
    `,
    [tenantId, JSON.stringify({
      appId: 'wx-worker-appid',
      merchantId: 'wx-worker-mchid',
      merchantSerialNo: 'wx-worker-serial',
      paymentQueryEndpoint: fakeWechat.paymentQueryEndpoint,
      refundEndpoint: fakeWechat.refundEndpoint,
      refundQueryEndpoint: fakeWechat.refundEndpoint,
      tradeBillEndpoint: fakeWechat.tradeBillEndpoint,
      secretRef: 'app_private.tenant_secrets:payment:wechat_pay',
    })],
  );

  await pool.query(
    `
    insert into public.tenant_payment_accounts (
      tenant_id, provider, mode, display_name, status, config_public
    )
    values ($1, 'alipay', 'tenant_collect', 'Worker Alipay', 'active', $2::jsonb)
    on conflict (tenant_id, provider)
    do update set mode = excluded.mode,
                  display_name = excluded.display_name,
                  status = excluded.status,
                  config_public = excluded.config_public,
                  updated_at = now()
    `,
    [tenantId, JSON.stringify({
      appId: 'ali-worker-appid',
      endpoint: fakeAlipay.endpoint,
      billEndpoint: fakeAlipay.endpoint,
      secretRef: 'app_private.tenant_secrets:payment:alipay',
    })],
  );

  await pool.query(
    `
    insert into public.orders (
      id, tenant_id, user_id, order_no, status, product_type, product_name,
      amount_cents, pay_method, pay_provider, plan_id, days, region_id, raw_payload,
      created_at, updated_at
    )
    values
      ($1, $2, $3, $4, 'pending', 'svip', 'Worker 补偿月卡', 990, 'jsapi', 'wechat_pay', $5, 30, $6, '{"source":"commerce-worker-test"}'::jsonb, now() - interval '10 minutes', now() - interval '10 minutes'),
      ($7, $2, $3, $8, 'paid', 'svip', 'Worker 退款月卡', 500, 'jsapi', 'wechat_pay', $5, 30, $6, '{"source":"commerce-worker-test"}'::jsonb, now() - interval '10 minutes', now() - interval '10 minutes'),
      ($9, $2, $3, $10, 'paid', 'svip', 'Worker 微信账单月卡', 990, 'jsapi', 'wechat_pay', $5, 30, $6, '{"source":"provider-bill-worker-test"}'::jsonb, now() - interval '10 minutes', now() - interval '10 minutes'),
      ($11, $2, $3, $12, 'paid', 'svip', 'Worker 支付宝账单月卡', 1288, 'wap', 'alipay', $5, 30, $6, '{"source":"provider-bill-worker-test"}'::jsonb, now() - interval '10 minutes', now() - interval '10 minutes')
    `,
    [
      ids.paymentOrder,
      tenantId,
      studentUserId,
      orderNos.payment,
      planId,
      regionId,
      ids.refundOrder,
      orderNos.refund,
      ids.providerBillOrderWechat,
      orderNos.providerBillWechat,
      ids.providerBillOrderAlipay,
      orderNos.providerBillAlipay,
    ],
  );

  await pool.query(
    `
    insert into public.payments (
      id, tenant_id, order_id, provider, method, status, amount_cents,
      provider_trade_no, paid_at, raw_payload, created_at, updated_at
    )
    values
      ($1, $2, $3, 'wechat_pay', 'jsapi', 'pending', 990, null, null, '{"source":"commerce-worker-test"}'::jsonb, now() - interval '10 minutes', now() - interval '10 minutes'),
      ($4, $2, $5, 'wechat_pay', 'jsapi', 'paid', 500, $6, now() - interval '9 minutes', '{"source":"commerce-worker-test"}'::jsonb, now() - interval '10 minutes', now() - interval '10 minutes'),
      ($7, $2, $8, 'wechat_pay', 'jsapi', 'paid', 990, $9, now() - interval '9 minutes', '{"source":"provider-bill-worker-test"}'::jsonb, now() - interval '10 minutes', now() - interval '10 minutes'),
      ($10, $2, $11, 'alipay', 'wap', 'paid', 1288, $12, now() - interval '9 minutes', '{"source":"provider-bill-worker-test"}'::jsonb, now() - interval '10 minutes', now() - interval '10 minutes')
    `,
    [
      ids.payment,
      tenantId,
      ids.paymentOrder,
      ids.refundPayment,
      ids.refundOrder,
      `wx-trade-${orderNos.refund}`,
      ids.providerBillPaymentWechat,
      ids.providerBillOrderWechat,
      `wx-bill-trade-${orderNos.providerBillWechat}`,
      ids.providerBillPaymentAlipay,
      ids.providerBillOrderAlipay,
      `ali-bill-trade-${orderNos.providerBillAlipay}`,
    ],
  );

  await pool.query(
    `
    insert into public.entitlements (
      id, tenant_id, user_id, entitlement_type, scope_type, scope_id,
      source_type, source_id, starts_at, expires_at, status, metadata
    )
    values ($1, $2, $3, 'svip', 'region', $4, 'order', $5, now() - interval '9 minutes', now() + interval '30 days', 'active', '{"source":"commerce-worker-test"}'::jsonb)
    `,
    [ids.refundEntitlement, tenantId, studentUserId, regionId, ids.refundOrder],
  );

  await pool.query(
    `
    insert into public.commerce_refund_requests (
      id, tenant_id, order_id, payment_id, refund_no, provider, provider_refund_no,
      status, amount_cents, reason, entitlement_action,
      requested_by, reviewed_by, processed_by,
      requested_at, reviewed_at, processed_at, metadata
    )
    values (
      $1, $2, $3, $4, $5, 'wechat_pay', null,
      'processing', 500, 'commerce worker integration refund', 'revoke_on_success',
      null, null, null,
      now() - interval '8 minutes', now() - interval '8 minutes', now() - interval '8 minutes',
      '{"source":"commerce-worker-test"}'::jsonb
    )
    `,
    [ids.refundRequest, tenantId, ids.refundOrder, ids.refundPayment, refundNo],
  );

  await pool.query(
    `
    insert into public.commerce_bill_download_jobs (
      id, tenant_id, provider, bill_date, bill_type, status,
      source_name, requested_by, metadata
    )
    values
      ($1, $3, 'wechat_pay', $4::date, 'payment', 'queued', $5, null, '{"source":"commerce-worker-test"}'::jsonb),
      ($2, $3, 'alipay', $4::date, 'payment', 'queued', $6, null, '{"source":"commerce-worker-test"}'::jsonb)
    on conflict (tenant_id, provider, bill_date, bill_type)
    where status in ('queued', 'running', 'completed')
    do update set status = 'queued',
                  source_name = excluded.source_name,
                  reconciliation_batch_id = null,
                  source_hash = null,
                  row_count = 0,
                  claimed_by = null,
                  claimed_at = null,
                  completed_at = null,
                  failed_at = null,
                  error_code = null,
                  error_message = null,
                  metadata = excluded.metadata,
                  updated_at = now()
    `,
    [
      ids.providerBillJobWechat,
      ids.providerBillJobAlipay,
      tenantId,
      providerBillDate,
      `provider-bill:wechat_pay:${providerBillDate}:payment`,
      `provider-bill:alipay:${providerBillDate}:payment`,
    ],
  );
}

async function main() {
  const fakeWechat = await startFakeWechatPayServer();
  const fakeAlipay = await startFakeAlipayServer();
  const pool = new pg.Pool({ connectionString: databaseUrl });
  let seeded = false;
  try {
    await pool.query('begin');
    await cleanup(pool);
    await seed(pool, fakeWechat, fakeAlipay);
    await pool.query('commit');
    seeded = true;

    const output = await runWorkerOnce();
    assert.match(output, /paid=1/, 'worker should reconcile one paid payment');
    assert.match(output, /succeeded=1/, 'worker should reconcile one succeeded refund');

    assert.ok(
      fakeWechat.requests.some(item => item.method === 'GET' && item.pathname.endsWith(`/${orderNos.payment}`)),
      'worker should query WeChat payment by out_trade_no',
    );
    assert.ok(
      fakeWechat.requests.some(item => item.method === 'GET' && item.pathname.endsWith(`/${refundNo}`)),
      'worker should query WeChat refund by out_refund_no',
    );

    const paymentOrder = await pool.query(
      `
      select o.status, o.trade_no, p.status as payment_status, p.provider_trade_no,
             count(e.id)::int as entitlement_count
      from public.orders o
      join public.payments p on p.tenant_id = o.tenant_id and p.order_id = o.id
      left join public.entitlements e on e.tenant_id = o.tenant_id and e.source_type = 'order' and e.source_id = o.id
      where o.tenant_id = $1 and o.id = $2
      group by o.status, o.trade_no, p.status, p.provider_trade_no
      `,
      [tenantId, ids.paymentOrder],
    );
    assert.equal(paymentOrder.rows[0]?.status, 'paid', 'payment compensation should mark order paid');
    assert.equal(paymentOrder.rows[0]?.payment_status, 'paid', 'payment compensation should mark payment paid');
    assert.equal(paymentOrder.rows[0]?.trade_no, `wx-trade-${orderNos.payment}`, 'payment compensation should record trade number');
    assert.equal(paymentOrder.rows[0]?.entitlement_count, 1, 'payment compensation should grant one order entitlement');

    const refund = await pool.query(
      `
      select rr.status, rr.provider_refund_no, o.status as order_status,
             o.refunded_amount_cents, p.status as payment_status,
             p.refunded_amount_cents as payment_refunded_amount_cents,
             e.status as entitlement_status
      from public.commerce_refund_requests rr
      join public.orders o on o.tenant_id = rr.tenant_id and o.id = rr.order_id
      join public.payments p on p.tenant_id = rr.tenant_id and p.id = rr.payment_id
      left join public.entitlements e on e.tenant_id = rr.tenant_id and e.source_type = 'order' and e.source_id = rr.order_id
      where rr.tenant_id = $1 and rr.id = $2
      `,
      [tenantId, ids.refundRequest],
    );
    assert.equal(refund.rows[0]?.status, 'succeeded', 'refund compensation should mark refund succeeded');
    assert.equal(refund.rows[0]?.provider_refund_no, `refund-${refundNo}`, 'refund compensation should record provider refund number');
    assert.equal(refund.rows[0]?.order_status, 'refunded', 'full refund compensation should mark order refunded');
    assert.equal(refund.rows[0]?.payment_status, 'refunded', 'full refund compensation should mark payment refunded');
    assert.equal(refund.rows[0]?.refunded_amount_cents, 500, 'full refund compensation should update order refunded amount');
    assert.equal(refund.rows[0]?.payment_refunded_amount_cents, 500, 'full refund compensation should update payment refunded amount');
    assert.equal(refund.rows[0]?.entitlement_status, 'revoked', 'full refund compensation should revoke order entitlement');

    await runWorkerOnce();
    const secondPass = await pool.query(
      `
      select count(*)::int as entitlement_count
      from public.entitlements
      where tenant_id = $1 and source_type = 'order' and source_id = $2
      `,
      [tenantId, ids.paymentOrder],
    );
    assert.equal(secondPass.rows[0]?.entitlement_count, 1, 'second worker pass must not grant duplicate entitlement');

    const events = await pool.query(
      `
      select payload, error
      from public.payment_events
      where tenant_id = $1
        and (
          event_id = $2
          or event_id = $3
        )
      order by created_at desc
      `,
      [
        tenantId,
        `${tenantId}:payment-query:${orderNos.payment}`,
        `${tenantId}:refund-query:${refundNo}`,
      ],
    );
    assert.ok(events.rows.length >= 2, 'worker should record payment events for reconciliation');
    assert.ok(!JSON.stringify(events.rows).includes(paymentFixture.wechatApiV3Key), 'payment event payload must not leak API v3 key');
    assert.ok(!JSON.stringify(events.rows).includes('PRIVATE KEY'), 'payment event payload must not leak private key');

    const providerBillOutput = await runProviderBillsWorkerOnce();
    assert.match(providerBillOutput, /completed=2/, 'provider bill worker should complete two provider bill jobs');
    assert.ok(
      fakeWechat.requests.some(item => item.method === 'GET' && item.pathname === '/v3/bill/tradebill'),
      'provider bill worker should request WeChat trade bill URL',
    );
    assert.ok(
      fakeWechat.requests.some(item => item.method === 'GET' && item.pathname === '/download/wechat-tradebill.json'),
      'provider bill worker should download WeChat trade bill',
    );
    assert.ok(
      fakeAlipay.requests.some(item => item.method === 'POST' && item.pathname === '/gateway.do' && item.body.includes('alipay.data.dataservice.bill.downloadurl.query')),
      'provider bill worker should request Alipay bill download URL',
    );
    assert.ok(
      fakeAlipay.requests.some(item => item.method === 'GET' && item.pathname === '/download/alipay-tradebill.json'),
      'provider bill worker should download Alipay trade bill',
    );

    const providerJobs = await pool.query(
      `
      select provider, status, source_hash, row_count, download_url_host,
             reconciliation_batch_id, error_code, error_message
      from public.commerce_bill_download_jobs
      where tenant_id = $1 and id in ($2::uuid, $3::uuid)
      order by provider
      `,
      [tenantId, ids.providerBillJobWechat, ids.providerBillJobAlipay],
    );
    assert.equal(providerJobs.rows.length, 2, 'provider bill jobs should be persisted');
    assert.ok(providerJobs.rows.every(row => row.status === 'completed'), 'provider bill jobs should be completed');
    assert.ok(providerJobs.rows.every(row => row.source_hash && !row.error_code && !row.error_message), 'completed provider bill jobs should keep source hash and no errors');
    assert.ok(providerJobs.rows.every(row => row.download_url_host === '127.0.0.1'), 'provider bill jobs should persist only download URL host');

    const providerBatches = await pool.query(
      `
      select b.id, b.provider, b.source, b.source_name, b.status,
             b.matched_count, b.missing_local_count, b.total_count,
             count(i.id)::int as item_count
      from public.commerce_reconciliation_batches b
      join public.commerce_reconciliation_items i
        on i.tenant_id = b.tenant_id and i.batch_id = b.id
      where b.tenant_id = $1
        and b.source = 'provider_download'
        and b.source_name like 'provider-bill:%:2026-06-29:%'
      group by b.id
      order by b.provider
      `,
      [tenantId],
    );
    assert.equal(providerBatches.rows.length, 2, 'provider bill downloads should create two reconciliation batches');
    assert.ok(
      providerBatches.rows.some(row => row.provider === 'wechat_pay' && row.status === 'completed_with_issues' && row.matched_count >= 1 && row.missing_local_count >= 1),
      'WeChat bill batch should include matched and provider-only anomaly rows',
    );
    assert.ok(
      providerBatches.rows.some(row => row.provider === 'alipay' && ['completed', 'completed_with_issues'].includes(row.status) && row.matched_count >= 1),
      'Alipay bill batch should include matched rows',
    );

    const jobPayload = JSON.stringify(providerJobs.rows);
    assert.ok(!jobPayload.includes(paymentFixture.wechatApiV3Key), 'provider bill job rows must not leak API v3 key');
    assert.ok(!jobPayload.includes('PRIVATE KEY'), 'provider bill job rows must not leak private keys');

    console.log('Commerce worker integration test complete.');
  } catch (error) {
    await pool.query('rollback').catch(() => {});
    throw error;
  } finally {
    if (seeded) {
      await cleanup(pool).catch(() => {});
    }
    await pool.end();
    await fakeWechat.close();
    await fakeAlipay.close();
  }
}

main().catch(error => {
  console.error(error);
  process.exit(1);
});
