#!/usr/bin/env tsx
/**
 * E2E: admin actions → in-app notification + email for workers & customers
 */
import fs from 'fs';
import path from 'path';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const API_BASE = (process.env.API_URL || 'http://localhost:4000').replace(/\/$/, '');
const API = API_BASE.endsWith('/api') ? API_BASE : `${API_BASE}/api`;
const PASS = process.env.DEMO_PASSWORD || 'Password123!';

let passed = 0;
let failed = 0;

function pass(label: string) {
  console.log(`  ✓ ${label}`);
  passed += 1;
}

function fail(label: string, detail?: string) {
  console.log(`  ✗ ${label}${detail ? `: ${detail}` : ''}`);
  failed += 1;
}

async function login(identifier: string) {
  const res = await fetch(`${API}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ identifier, password: PASS }),
  });
  const data = await res.json();
  if (!data.success) throw new Error(data.error || `login failed for ${identifier}`);
  return data.data.accessToken as string;
}

async function api<T>(
  token: string | null,
  method: string,
  endpoint: string,
  body?: unknown
): Promise<T> {
  const res = await fetch(`${API}${endpoint}`, {
    method,
    headers: {
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  return (await res.json()) as T;
}

function createTestPng(filePath: string) {
  const png = Buffer.from(
    'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
    'base64'
  );
  fs.writeFileSync(filePath, png);
}

async function uploadDoc(token: string, type: string, filePath: string) {
  const form = new FormData();
  const blob = new Blob([fs.readFileSync(filePath)], { type: 'image/png' });
  form.append('document', blob, path.basename(filePath));
  form.append('type', type);
  const res = await fetch(`${API}/verifications`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}` },
    body: form,
  });
  const data = await res.json();
  if (!data.success) throw new Error(data.error || 'upload failed');
  return data.data as { id: string; type: string };
}

async function readOtp(destination: string, purpose: string) {
  const row = await prisma.oTPVerification.findFirst({
    where: { phone: destination.toLowerCase(), purpose, isUsed: false },
    orderBy: { createdAt: 'desc' },
  });
  if (!row?.code) throw new Error(`OTP not found for ${destination}/${purpose}`);
  return row.code;
}

async function latestNotification(userId: string, since: Date) {
  return prisma.notification.findFirst({
    where: { userId, createdAt: { gte: since } },
    orderBy: { createdAt: 'desc' },
  });
}

async function expectNotification(
  label: string,
  userId: string,
  since: Date,
  match: { type?: string; titleIncludes?: string }
) {
  // allow slight clock skew / async write
  await new Promise((r) => setTimeout(r, 150));
  const note = await latestNotification(userId, new Date(since.getTime() - 1000));
  if (!note) {
    fail(label, 'no notification created');
    return null;
  }
  if (match.type && note.type !== match.type) {
    fail(label, `type=${note.type}`);
    return note;
  }
  if (match.titleIncludes && !note.title.includes(match.titleIncludes)) {
    fail(label, `title="${note.title}"`);
    return note;
  }
  pass(label);
  return note;
}

async function registerWorker(stamp: number, categoryId: string) {
  const email = `notify.worker.${stamp}@ehandyhire.test`;
  const phone = `+63918${String(stamp).slice(-7)}`;
  await api(null, 'POST', '/auth/worker/send-otp', { email });
  const otp = await readOtp(email, 'worker_register');
  const reg = await api<{
    success: boolean;
    error?: string;
    data: { accessToken: string; user: { id: string } };
  }>(null, 'POST', '/auth/register', {
    firstName: 'Notify',
    lastName: 'Worker',
    displayName: 'Mang Notify',
    email,
    password: PASS,
    role: 'WORKER',
    gender: 'MALE',
    otpCode: otp,
    serviceAreas: ['DAVAO_CITY'],
    categoryIds: [categoryId],
  });
  if (!reg.success) throw new Error(reg.error || 'worker register failed');

  const me = await api<{
    success: boolean;
    data: { id: string; workerProfile: { id: string } };
  }>(reg.data.accessToken, 'GET', '/auth/me');

  // verify phone so full verification can unlock
  await api(reg.data.accessToken, 'POST', '/auth/phone/send-otp', { phone });
  const phoneOtp = await readOtp(phone, 'phone_verify');
  await api(reg.data.accessToken, 'POST', '/auth/phone/verify', {
    phone,
    otpCode: phoneOtp,
  });

  return {
    email,
    phone,
    token: reg.data.accessToken,
    userId: me.data.id,
    workerId: me.data.workerProfile.id,
  };
}

async function registerCustomer(stamp: number) {
  const email = `notify.customer.${stamp}@ehandyhire.test`;
  const phone = `+63919${String(stamp).slice(-7)}`;
  await api(null, 'POST', '/auth/customer/send-otp', { email });
  const otp = await readOtp(email, 'customer_register');
  const reg = await api<{
    success: boolean;
    error?: string;
    data: { accessToken: string };
  }>(null, 'POST', '/auth/register', {
    firstName: 'Notify',
    lastName: 'Customer',
    email,
    password: PASS,
    role: 'CUSTOMER',
    otpCode: otp,
  });
  if (!reg.success) throw new Error(reg.error || 'customer register failed');

  const me = await api<{
    success: boolean;
    data: { id: string; customerProfile: { id: string } };
  }>(reg.data.accessToken, 'GET', '/auth/me');

  await api(reg.data.accessToken, 'POST', '/auth/phone/send-otp', { phone });
  const phoneOtp = await readOtp(phone, 'phone_verify');
  await api(reg.data.accessToken, 'POST', '/auth/phone/verify', {
    phone,
    otpCode: phoneOtp,
  });

  return {
    email,
    phone,
    token: reg.data.accessToken,
    userId: me.data.id,
    customerId: me.data.customerProfile.id,
  };
}

async function main() {
  console.log('=== Admin notify + email E2E (worker + customer) ===\n');

  const stamp = Date.now();
  const tmpPng = path.join(process.cwd(), 'uploads', 'test-notify.png');
  fs.mkdirSync(path.dirname(tmpPng), { recursive: true });
  createTestPng(tmpPng);

  const cats = await api<{ success: boolean; data: { id: string }[] }>(
    null,
    'GET',
    '/categories?limit=1'
  );
  if (!cats.data?.[0]?.id) throw new Error('API/categories unavailable');

  const adminToken = await login('admin@ehandyhire.com');
  pass('Admin login');

  const worker = await registerWorker(stamp, cats.data[0].id);
  pass(`Worker registered (${worker.email})`);
  const customer = await registerCustomer(stamp);
  pass(`Customer registered (${customer.email})`);

  // ─── Worker document reject ─────────────────────────────────────────────
  console.log('\n[WORKER] document reject → notify + email');
  {
    const since = new Date();
    const badId = await uploadDoc(worker.token, 'GOVERNMENT_ID', tmpPng);
    const review = await api<{ success: boolean; data: { notification: { id: string } } }>(
      adminToken,
      'PATCH',
      `/verifications/${badId.id}/review`,
      { status: 'REJECTED', adminNote: 'Please upload a clearer photo' }
    );
    if (review.success && review.data?.notification?.id) pass('Reject API returns notification');
    else fail('Reject API notification payload');

    await expectNotification('Worker reject in-app notification', worker.userId, since, {
      type: 'VERIFICATION_UPDATE',
      titleIncludes: 'rejected',
    });
  }

  // ─── Worker docs approve → fully verified ───────────────────────────────
  console.log('\n[WORKER] docs approved → fully verified notify');
  {
    const since = new Date();
    const idDoc = await uploadDoc(worker.token, 'GOVERNMENT_ID', tmpPng);
    const selfie = await uploadDoc(worker.token, 'SELFIE', tmpPng);
    await api(adminToken, 'PATCH', `/verifications/${idDoc.id}/review`, {
      status: 'APPROVED',
    });
    await api(adminToken, 'PATCH', `/verifications/${selfie.id}/review`, {
      status: 'APPROVED',
    });

    const me = await api<{
      success: boolean;
      data: { workerProfile: { isVerified: boolean } };
    }>(worker.token, 'GET', '/auth/me');
    if (me.data.workerProfile.isVerified) pass('Worker profile fully verified');
    else fail('Worker should be fully verified');

    await expectNotification('Worker fully-verified in-app notification', worker.userId, since, {
      type: 'VERIFICATION_UPDATE',
      titleIncludes: 'verified',
    });
  }

  // ─── Worker manual unverify / verify ────────────────────────────────────
  console.log('\n[WORKER] manual unverify / verify');
  {
    let since = new Date();
    await api(adminToken, 'PATCH', `/workers/admin/${worker.workerId}`, {
      isVerified: false,
    });
    await expectNotification('Worker unverify notification', worker.userId, since, {
      type: 'VERIFICATION_UPDATE',
      titleIncludes: 'removed',
    });

    since = new Date();
    await api(adminToken, 'PATCH', `/workers/admin/${worker.workerId}`, {
      isVerified: true,
    });
    await expectNotification('Worker manual verify notification', worker.userId, since, {
      type: 'VERIFICATION_UPDATE',
      titleIncludes: 'verified by admin',
    });
  }

  // ─── Worker availability ────────────────────────────────────────────────
  console.log('\n[WORKER] availability toggle');
  {
    const since = new Date();
    await api(adminToken, 'PATCH', `/workers/admin/${worker.workerId}`, {
      isAvailable: false,
    });
    await expectNotification('Worker unavailable notification', worker.userId, since, {
      type: 'SYSTEM',
      titleIncludes: 'unavailable',
    });
    await api(adminToken, 'PATCH', `/workers/admin/${worker.workerId}`, {
      isAvailable: true,
    });
  }

  // ─── Customer document additional docs + approve ────────────────────────
  console.log('\n[CUSTOMER] request more docs → approve → fully verified');
  {
    let since = new Date();
    const idDoc = await uploadDoc(customer.token, 'GOVERNMENT_ID', tmpPng);
    await api(adminToken, 'PATCH', `/verifications/${idDoc.id}/review`, {
      status: 'ADDITIONAL_DOCS_REQUIRED',
      adminNote: 'Need a clearer ID scan',
    });
    await expectNotification('Customer more-docs notification', customer.userId, since, {
      type: 'VERIFICATION_UPDATE',
      titleIncludes: 'More documents',
    });

    since = new Date();
    const id2 = await uploadDoc(customer.token, 'GOVERNMENT_ID', tmpPng);
    const selfie = await uploadDoc(customer.token, 'SELFIE', tmpPng);
    await api(adminToken, 'PATCH', `/verifications/${id2.id}/review`, { status: 'APPROVED' });
    await api(adminToken, 'PATCH', `/verifications/${selfie.id}/review`, { status: 'APPROVED' });

    const me = await api<{
      success: boolean;
      data: { customerProfile: { isVerified: boolean } };
    }>(customer.token, 'GET', '/auth/me');
    if (me.data.customerProfile.isVerified) pass('Customer profile fully verified');
    else fail('Customer should be fully verified');

    await expectNotification('Customer fully-verified notification', customer.userId, since, {
      type: 'VERIFICATION_UPDATE',
      titleIncludes: 'verified',
    });
  }

  // ─── Suspend / activate (customer) ──────────────────────────────────────
  console.log('\n[CUSTOMER] suspend / activate');
  {
    let since = new Date();
    await api(adminToken, 'PATCH', `/users/${customer.userId}/status`, {
      status: 'SUSPENDED',
    });
    await expectNotification('Customer suspend notification', customer.userId, since, {
      type: 'SYSTEM',
      titleIncludes: 'suspended',
    });

    since = new Date();
    await api(adminToken, 'PATCH', `/users/${customer.userId}/status`, {
      status: 'ACTIVE',
    });
    await expectNotification('Customer activate notification', customer.userId, since, {
      type: 'SYSTEM',
      titleIncludes: 'reactivated',
    });
  }

  // ─── Suspend / activate (worker) ────────────────────────────────────────
  console.log('\n[WORKER] suspend / activate');
  {
    let since = new Date();
    await api(adminToken, 'PATCH', `/users/${worker.userId}/status`, {
      status: 'SUSPENDED',
    });
    await expectNotification('Worker suspend notification', worker.userId, since, {
      type: 'SYSTEM',
      titleIncludes: 'suspended',
    });

    since = new Date();
    await api(adminToken, 'PATCH', `/users/${worker.userId}/status`, {
      status: 'ACTIVE',
    });
    await expectNotification('Worker activate notification', worker.userId, since, {
      type: 'SYSTEM',
      titleIncludes: 'reactivated',
    });
  }

  // Count notifications created for each user in this run window
  const workerNotes = await prisma.notification.count({
    where: { userId: worker.userId, createdAt: { gte: new Date(stamp - 5000) } },
  });
  const customerNotes = await prisma.notification.count({
    where: { userId: customer.userId, createdAt: { gte: new Date(stamp - 5000) } },
  });
  if (workerNotes >= 5) pass(`Worker received ${workerNotes} notifications`);
  else fail('Worker notification count too low', String(workerNotes));
  if (customerNotes >= 4) pass(`Customer received ${customerNotes} notifications`);
  else fail('Customer notification count too low', String(customerNotes));

  console.log('\n── Test accounts (Password123!) ──');
  console.log(`  Worker:   ${worker.email}`);
  console.log(`  Customer: ${customer.email}`);
  console.log('  (Emails logged in API console when RESEND_API_KEY is unset)');

  console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
  if (failed > 0) process.exit(1);
  console.log('=== ALL PASSED ===\n');
}

main()
  .catch((e) => {
    console.error('\n=== FAILED ===');
    console.error(e);
    process.exit(1);
  })
  .finally(() => prisma.$disconnect());
