#!/usr/bin/env tsx
/**
 * E2E: customer + worker email OTP registration, portal phone verify, doc approval.
 */
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 };
}

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 me(token: string) {
  const res = await api<{
    success: boolean;
    data: {
      email: string | null;
      phone: string | null;
      emailVerified: boolean;
      phoneVerified: boolean;
      role: string;
      customerProfile?: { id: string; isVerified: boolean; verificationStatus: string } | null;
      workerProfile?: { id: string; isVerified: boolean; verificationStatus: string } | null;
    };
  }>(token, 'GET', '/auth/me');
  if (!res.success) throw new Error('auth/me failed');
  return res.data;
}

async function verifyPhoneInPortal(token: string, phone: string) {
  const send = await api<{ success: boolean; error?: string }>(token, 'POST', '/auth/phone/send-otp', {
    phone,
  });
  if (!send.success) throw new Error(send.error || 'phone send-otp failed');
  const code = await readOtp(phone, 'phone_verify');
  const verify = await api<{ success: boolean; data: { phoneVerified: boolean }; error?: string }>(
    token,
    'POST',
    '/auth/phone/verify',
    { phone, otpCode: code }
  );
  if (!verify.success || !verify.data.phoneVerified) {
    throw new Error(verify.error || 'phone verify failed');
  }
}

async function approveDocs(adminToken: string, userToken: string) {
  const idDoc = await uploadDoc(userToken, 'GOVERNMENT_ID', path.join(process.cwd(), 'uploads', 'test-id.png'));
  const selfieDoc = await uploadDoc(userToken, 'SELFIE', path.join(process.cwd(), 'uploads', 'test-id.png'));
  await api(adminToken, 'PATCH', `/verifications/${idDoc.id}/review`, { status: 'APPROVED' });
  await api(adminToken, 'PATCH', `/verifications/${selfieDoc.id}/review`, { status: 'APPROVED' });
}

async function runWorkerFlow(stamp: number, categoryId: string, adminToken: string) {
  console.log('\n[WORKER] email OTP → docs → phone → verified');
  const email = `e2e.worker.${stamp}@ehandyhire.test`;
  const phone = `+63918${String(stamp).slice(-7)}`;

  const send = await api<{ success: boolean; error?: string }>(null, 'POST', '/auth/worker/send-otp', {
    email,
  });
  if (!send.success) throw new Error(send.error || 'worker send-otp failed');
  pass('Worker email OTP sent');

  const otpCode = await readOtp(email, 'worker_register');
  pass(`Worker email OTP retrieved (${otpCode})`);

  const reg = await api<{
    success: boolean;
    error?: string;
    data: { accessToken: string };
  }>(null, 'POST', '/auth/register', {
    firstName: 'E2E',
    lastName: 'Worker',
    displayName: 'Mang E2E',
    email,
    password: PASS,
    role: 'WORKER',
    gender: 'MALE',
    otpCode,
    serviceAreas: ['DAVAO_CITY'],
    categoryIds: [categoryId],
  });
  if (!reg.success) throw new Error(reg.error || 'worker register failed');
  const token = reg.data.accessToken;
  pass(`Worker registered (${email})`);

  let profile = await me(token);
  if (profile.emailVerified && !profile.phoneVerified && !profile.phone && !profile.workerProfile?.isVerified) {
    pass('Worker starts: email verified, no phone, unverified profile');
  } else {
    fail('Unexpected worker start state', JSON.stringify(profile));
  }

  // Docs before phone must not fully verify
  await approveDocs(adminToken, token);
  pass('Worker docs approved by admin');
  profile = await me(token);
  if (!profile.workerProfile?.isVerified) pass('Worker still unverified without phone');
  else fail('Worker should not verify from docs alone');

  await verifyPhoneInPortal(token, phone);
  pass('Worker phone verified in portal');

  profile = await me(token);
  if (
    profile.phoneVerified &&
    profile.workerProfile?.isVerified &&
    profile.workerProfile.verificationStatus === 'APPROVED'
  ) {
    pass('Worker fully verified (phone + docs)');
  } else {
    fail('Worker not fully verified', JSON.stringify({
      phoneVerified: profile.phoneVerified,
      isVerified: profile.workerProfile?.isVerified,
      status: profile.workerProfile?.verificationStatus,
    }));
  }

  const pub = await api<{ success: boolean; data?: { id: string } }>(
    null,
    'GET',
    `/workers/${profile.workerProfile!.id}`
  );
  if (pub.success && pub.data?.id === profile.workerProfile!.id) {
    pass('Verified worker is publicly visible');
  } else {
    fail('Verified worker should be public');
  }

  await login(email);
  pass('Worker can log in with email');

  return { email, phone };
}

async function runCustomerFlow(stamp: number, adminToken: string) {
  console.log('\n[CUSTOMER] email OTP → docs → phone → verified');
  const email = `e2e.customer.${stamp}@ehandyhire.test`;
  const phone = `+63919${String(stamp).slice(-7)}`;

  const send = await api<{ success: boolean; error?: string }>(null, 'POST', '/auth/customer/send-otp', {
    email,
  });
  if (!send.success) throw new Error(send.error || 'customer send-otp failed');
  pass('Customer email OTP sent');

  const otpCode = await readOtp(email, 'customer_register');
  pass(`Customer email OTP retrieved (${otpCode})`);

  const reg = await api<{
    success: boolean;
    error?: string;
    data: { accessToken: string };
  }>(null, 'POST', '/auth/register', {
    firstName: 'E2E',
    lastName: 'Customer',
    email,
    password: PASS,
    role: 'CUSTOMER',
    otpCode,
  });
  if (!reg.success) throw new Error(reg.error || 'customer register failed');
  const token = reg.data.accessToken;
  pass(`Customer registered (${email})`);

  let profile = await me(token);
  if (
    profile.emailVerified &&
    !profile.phoneVerified &&
    !profile.phone &&
    !profile.customerProfile?.isVerified
  ) {
    pass('Customer starts: email verified, no phone, unverified profile');
  } else {
    fail('Unexpected customer start state', JSON.stringify(profile));
  }

  await approveDocs(adminToken, token);
  pass('Customer docs approved by admin');
  profile = await me(token);
  if (!profile.customerProfile?.isVerified) pass('Customer still unverified without phone');
  else fail('Customer should not verify from docs alone');

  await verifyPhoneInPortal(token, phone);
  pass('Customer phone verified in portal');

  profile = await me(token);
  if (
    profile.phoneVerified &&
    profile.customerProfile?.isVerified &&
    profile.customerProfile.verificationStatus === 'APPROVED'
  ) {
    pass('Customer fully verified (phone + docs)');
  } else {
    fail('Customer not fully verified', JSON.stringify({
      phoneVerified: profile.phoneVerified,
      isVerified: profile.customerProfile?.isVerified,
      status: profile.customerProfile?.verificationStatus,
    }));
  }

  await login(email);
  pass('Customer can log in with email');

  // Unverified customer gate: register a second customer without phone and confirm hire is blocked
  // (optional lightweight check via /auth/me only — hire needs a worker)
  return { email, phone };
}

async function main() {
  console.log('=== Customer + Worker registration/verification E2E ===');

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

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

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

  const worker = await runWorkerFlow(stamp, catRes.data[0].id, adminToken);
  const customer = await runCustomerFlow(stamp, adminToken);

  // Reject-path regression for worker: old rejected ID must not block after replacement
  console.log('\n[WORKER] reject → re-upload → phone+docs verify');
  const rejectEmail = `e2e.reject.${stamp}@ehandyhire.test`;
  const rejectPhone = `+63917${String(stamp).slice(-7)}`;
  await api(null, 'POST', '/auth/worker/send-otp', { email: rejectEmail });
  const rejectOtp = await readOtp(rejectEmail, 'worker_register');
  const rejectReg = await api<{ success: boolean; data: { accessToken: string }; error?: string }>(
    null,
    'POST',
    '/auth/register',
    {
      firstName: 'Reject',
      lastName: 'Path',
      displayName: 'Kuya Reject',
      email: rejectEmail,
      password: PASS,
      role: 'WORKER',
      gender: 'MALE',
      otpCode: rejectOtp,
      serviceAreas: ['DAVAO_CITY'],
      categoryIds: [catRes.data[0].id],
    }
  );
  if (!rejectReg.success) throw new Error(rejectReg.error || 'reject worker register failed');
  const rejectToken = rejectReg.data.accessToken;
  pass(`Reject-path worker registered (${rejectEmail})`);

  await verifyPhoneInPortal(rejectToken, rejectPhone);
  pass('Reject-path phone verified first');

  const badId = await uploadDoc(rejectToken, 'GOVERNMENT_ID', tmpPng);
  const selfie = await uploadDoc(rejectToken, 'SELFIE', tmpPng);
  await api(adminToken, 'PATCH', `/verifications/${badId.id}/review`, {
    status: 'REJECTED',
    adminNote: 'blurry',
  });
  pass('First ID rejected');

  const goodId = await uploadDoc(rejectToken, 'GOVERNMENT_ID', tmpPng);
  await api(adminToken, 'PATCH', `/verifications/${selfie.id}/review`, { status: 'APPROVED' });
  await api(adminToken, 'PATCH', `/verifications/${goodId.id}/review`, { status: 'APPROVED' });
  pass('Replacement ID + selfie approved');

  const rejectMe = await me(rejectToken);
  if (rejectMe.workerProfile?.isVerified) {
    pass('Reject-path worker verified despite old rejected doc');
  } else {
    fail('Reject-path should be verified');
  }

  console.log('\n── Test accounts (Password123!) ──');
  console.log(`  Worker:   ${worker.email}  phone ${worker.phone}`);
  console.log(`  Customer: ${customer.email}  phone ${customer.phone}`);
  console.log(`  Reject→OK:${rejectEmail}  phone ${rejectPhone}`);

  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());
