import assert from 'node:assert/strict';
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 MAIN_TENANT_ID = '00000000-0000-0000-0000-000000000001';
const PARTNER_TENANT_ID = '00000000-0000-0000-0000-000000000901';
const PARTNER_ADMIN_USER_ID = '00000000-0000-0000-0000-000000000907';

const ids = {
  sourceQuestionBank: '00000000-0000-0000-0000-000000000400',
  grant: '00000000-0000-0000-0000-000000000906',
  targetQuestionBank: '00000000-0000-0000-0000-00000000c901',
  targetEntry: '00000000-0000-0000-0000-00000000c902',
  targetCollection: '00000000-0000-0000-0000-00000000c903',
  adoption: '00000000-0000-0000-0000-00000000c904',
};

function runWorkerOnce() {
  const child = spawn(process.execPath, ['apps/worker/dist/apps/worker/src/index.js', '--once', '--job', 'public-banks'], {
    cwd: process.cwd(),
    env: {
      ...process.env,
      DATABASE_URL: databaseUrl,
      WORKER_PUBLIC_BANK_SYNC_BATCH_SIZE: '5',
      WORKER_PUBLIC_BANK_SYNC_COPY_LIMIT: '2',
      WORKER_PUBLIC_BANK_SYNC_ID: 'public-bank-sync-integration-test',
    },
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true,
  });
  let output = '';
  child.stdout.on('data', chunk => {
    output += chunk.toString();
  });
  child.stderr.on('data', chunk => {
    output += chunk.toString();
  });
  return new Promise((resolve, reject) => {
    child.on('error', reject);
    child.on('exit', code => {
      try {
        assert.equal(code, 0, `worker should exit 0\n${output}`);
        assert.match(output, /public-banks batch processed=\d+/, 'worker output should include public bank sync summary');
        resolve(output);
      } catch (error) {
        reject(error);
      }
    });
  });
}

async function cleanup(pool) {
  await pool.query('delete from public.audit_logs where tenant_id = $1 and target_id = $2', [PARTNER_TENANT_ID, ids.adoption]);
  await pool.query('delete from public.tenant_question_bank_adoptions where tenant_id = $1 and source_question_bank_id = $2', [PARTNER_TENANT_ID, ids.sourceQuestionBank]);
  await pool.query(
    `
    delete from public.question_collection_items
    where tenant_id = $1
      and (collection_id = $2 or metadata->>'source' = 'public_question_bank_adoption')
    `,
    [PARTNER_TENANT_ID, ids.targetCollection],
  );
  await pool.query(
    `
    delete from public.question_versions
    where tenant_id = $1
      and question_id in (
        select id
        from public.questions
        where tenant_id = $1 and legacy_id like 'public:%'
      )
    `,
    [PARTNER_TENANT_ID],
  );
  await pool.query('delete from public.questions where tenant_id = $1 and legacy_id like $2', [PARTNER_TENANT_ID, 'public:%']);
  await pool.query('delete from public.question_collections where tenant_id = $1 and id = $2', [PARTNER_TENANT_ID, ids.targetCollection]);
  await pool.query('delete from public.content_entries where tenant_id = $1 and id = $2', [PARTNER_TENANT_ID, ids.targetEntry]);
  await pool.query('delete from public.question_banks where tenant_id = $1 and id = $2', [PARTNER_TENANT_ID, ids.targetQuestionBank]);
}

async function createPendingAdoption(pool) {
  await pool.query(
    `
    insert into public.question_banks (id, tenant_id, name, source_scope, status, metadata)
    values ($1, $2, 'worker 自动同步目标题库', 'tenant', 'active', '{"source":"public_bank_worker_test"}'::jsonb)
    `,
    [ids.targetQuestionBank, PARTNER_TENANT_ID],
  );
  await pool.query(
    `
    insert into public.content_entries (
      id, tenant_id, entry_key, name, entry_type, icon, route,
      description, visibility, access_rules, layout_config, sort_order,
      is_active, created_by, updated_by
    )
    values (
      $1, $2, 'worker-public-bank-sync', 'worker 自动同步题库入口',
      'question_practice', 'book-open', '/practice',
      'worker integration public bank sync', 'public', '{}'::jsonb,
      '{}'::jsonb, 100, true, $3, $3
    )
    `,
    [ids.targetEntry, PARTNER_TENANT_ID, PARTNER_ADMIN_USER_ID],
  );
  await pool.query(
    `
    insert into public.question_collections (
      id, tenant_id, entry_id, question_bank_id, name,
      collection_type, source_type, filters, question_count,
      status, sort_order, metadata, created_by, updated_by
    )
    values (
      $1, $2, $3, $4, 'worker 自动同步题目集合',
      'manual', 'manual_questions', '{}'::jsonb, 0,
      'active', 1, '{"source":"public_question_bank_adoption"}'::jsonb, $5, $5
    )
    `,
    [ids.targetCollection, PARTNER_TENANT_ID, ids.targetEntry, ids.targetQuestionBank, PARTNER_ADMIN_USER_ID],
  );
  await pool.query(
    `
    insert into public.tenant_question_bank_adoptions (
      id, tenant_id, source_question_bank_id, grant_id,
      target_question_bank_id, target_entry_id, target_collection_id,
      adoption_mode, status, sync_status, source_snapshot,
      copied_question_count, metadata, created_by, updated_by
    )
    values (
      $1, $2, $3, $4,
      $5, $6, $7,
      'copied_snapshot', 'active', 'pending', '{}'::jsonb,
      0, '{"source":"public_bank_worker_test"}'::jsonb, $8, $8
    )
    `,
    [
      ids.adoption,
      PARTNER_TENANT_ID,
      ids.sourceQuestionBank,
      ids.grant,
      ids.targetQuestionBank,
      ids.targetEntry,
      ids.targetCollection,
      PARTNER_ADMIN_USER_ID,
    ],
  );
}

async function main() {
  const pool = new pg.Pool({ connectionString: databaseUrl });
  try {
    await cleanup(pool);
    await createPendingAdoption(pool);

    const output = await runWorkerOnce();
    assert.match(output, /processed=1/, 'worker should claim the pending public bank adoption');
    assert.match(output, /synced=1/, 'worker should sync the pending public bank adoption');

    const adoption = await pool.query(
      `
      select status, sync_status, copied_question_count, metadata, last_synced_at
      from public.tenant_question_bank_adoptions
      where tenant_id = $1 and id = $2
      `,
      [PARTNER_TENANT_ID, ids.adoption],
    );
    assert.equal(adoption.rows[0]?.status, 'active', 'worker should return adoption to active status');
    assert.equal(adoption.rows[0]?.sync_status, 'synced', 'worker should mark adoption synced');
    assert.equal(Number(adoption.rows[0]?.copied_question_count), 2, 'worker should respect configured copy limit');
    assert.equal(adoption.rows[0]?.metadata?.lastSync?.triggeredBy, 'worker', 'metadata should record worker-triggered sync');
    assert.ok(adoption.rows[0]?.last_synced_at, 'worker should record last_synced_at');

    const copied = await pool.query(
      `
      select q.id, q.legacy_id, v.content
      from public.questions q
      join public.question_versions v on v.id = q.current_version_id
      where q.tenant_id = $1
        and q.legacy_id like $2
      order by q.created_at asc
      `,
      [PARTNER_TENANT_ID, `public:${MAIN_TENANT_ID}:%`],
    );
    assert.equal(copied.rowCount, 2, 'worker should create copied tenant questions');
    assert.ok(copied.rows.every(row => row.content), 'copied questions should have current version content');

    const collectionItems = await pool.query(
      `
      select count(*)::integer as count
      from public.question_collection_items
      where tenant_id = $1 and collection_id = $2
      `,
      [PARTNER_TENANT_ID, ids.targetCollection],
    );
    assert.equal(Number(collectionItems.rows[0]?.count), 2, 'worker should bind copied questions to target collection');

    const audit = await pool.query(
      `
      select action, details
      from public.audit_logs
      where tenant_id = $1 and target_id = $2 and target_type = 'tenant_question_bank_adoption'
      order by created_at desc
      limit 1
      `,
      [PARTNER_TENANT_ID, ids.adoption],
    );
    assert.equal(audit.rows[0]?.action, 'content.public_question_bank.synced', 'worker should write sync audit');
    assert.equal(audit.rows[0]?.details?.triggeredBy, 'worker', 'worker audit should record trigger source');

    const notifications = await pool.query(
      `
      select notification_type, status, severity, metadata
      from public.tenant_content_notifications
      where tenant_id = $1 and adoption_id = $2
      order by created_at desc
      limit 5
      `,
      [PARTNER_TENANT_ID, ids.adoption],
    );
    assert.ok(
      notifications.rows.some(row => row.notification_type === 'public_question_bank_synced' && row.status === 'unread' && row.severity === 'success'),
      'worker public bank sync should create an unread success notification',
    );
    assert.equal(
      notifications.rows.find(row => row.notification_type === 'public_question_bank_synced')?.metadata?.triggeredBy,
      'worker',
      'worker public bank sync notification should record trigger source',
    );

    const secondOutput = await runWorkerOnce();
    assert.match(secondOutput, /processed=0/, 'worker should skip already synced public bank when source has not changed');

    await pool.query(
      'update public.question_bank_grants set status = $2, updated_at = now() where id = $1',
      [ids.grant, 'disabled'],
    );
    await pool.query(
      `
      update public.tenant_question_bank_adoptions
      set status = 'active',
          sync_status = 'pending',
          metadata = metadata || '{"source":"public_bank_worker_failure_test"}'::jsonb,
          updated_at = now()
      where tenant_id = $1 and id = $2
      `,
      [PARTNER_TENANT_ID, ids.adoption],
    );

    const failedOutput = await runWorkerOnce();
    assert.match(failedOutput, /processed=1/, 'worker should claim forced pending adoption');
    assert.match(failedOutput, /failed=1/, 'worker should count unavailable grant as failed');

    const failedAdoption = await pool.query(
      `
      select status, sync_status, metadata
      from public.tenant_question_bank_adoptions
      where tenant_id = $1 and id = $2
      `,
      [PARTNER_TENANT_ID, ids.adoption],
    );
    assert.equal(failedAdoption.rows[0]?.status, 'active', 'failed worker sync should return adoption to active');
    assert.equal(failedAdoption.rows[0]?.sync_status, 'failed', 'failed worker sync should mark adoption failed');
    assert.equal(
      failedAdoption.rows[0]?.metadata?.publicBankSyncWorker?.lastError?.code,
      'QUESTION_BANK_GRANT_NOT_AVAILABLE',
      'failed worker sync should persist a stable error code',
    );

    const failureNotifications = await pool.query(
      `
      select notification_type, status, severity, message, metadata
      from public.tenant_content_notifications
      where tenant_id = $1 and adoption_id = $2 and notification_type = 'public_question_bank_sync_failed'
      order by created_at desc
      limit 5
      `,
      [PARTNER_TENANT_ID, ids.adoption],
    );
    const failureNotification = failureNotifications.rows[0];
    assert.equal(failureNotification?.status, 'unread', 'failed worker sync should create an unread tenant notification');
    assert.equal(failureNotification?.severity, 'error', 'failed worker sync notification should use error severity');
    assert.equal(
      failureNotification?.metadata?.errorCode,
      'QUESTION_BANK_GRANT_NOT_AVAILABLE',
      'failed worker sync notification should expose stable error code metadata',
    );
    assert.ok(
      !JSON.stringify(failureNotification).includes('integration-second-region-v1'),
      'failed worker sync notification should not leak source question details',
    );

    await pool.query(
      'update public.question_bank_grants set status = $2, updated_at = now() where id = $1',
      [ids.grant, 'active'],
    );
    await pool.query(
      `
      update public.tenant_question_bank_adoptions
      set status = 'active', sync_status = 'pending', updated_at = now()
      where tenant_id = $1 and id = $2
      `,
      [PARTNER_TENANT_ID, ids.adoption],
    );

    const recoveryOutput = await runWorkerOnce();
    assert.match(recoveryOutput, /processed=1/, 'worker should retry adoption after grant recovers');

    const recoveredAdoption = await pool.query(
      `
      select sync_status
      from public.tenant_question_bank_adoptions
      where tenant_id = $1 and id = $2
      `,
      [PARTNER_TENANT_ID, ids.adoption],
    );
    assert.equal(recoveredAdoption.rows[0]?.sync_status, 'synced', 'worker should recover failed sync after grant becomes available');

    const resolvedFailureNotifications = await pool.query(
      `
      select status, resolved_at
      from public.tenant_content_notifications
      where tenant_id = $1 and adoption_id = $2 and notification_type = 'public_question_bank_sync_failed'
      order by updated_at desc
      limit 1
      `,
      [PARTNER_TENANT_ID, ids.adoption],
    );
    assert.equal(
      resolvedFailureNotifications.rows[0]?.status,
      'resolved',
      'successful recovery should resolve previous public bank sync failure notifications',
    );
    assert.ok(resolvedFailureNotifications.rows[0]?.resolved_at, 'resolved failure notification should record resolved_at');

    console.log('Public question bank sync worker integration test complete.');
  } finally {
    await pool.query('update public.question_bank_grants set status = $2, updated_at = now() where id = $1', [ids.grant, 'active']).catch(() => {});
    await cleanup(pool).catch(() => {});
    await pool.end();
  }
}

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