/**
 * test-mail.ts — Shared helper for live email integration tests.
 *
 * Provides:
 *   - provisionInbox()          creates a fresh mail.tm inbox (anonymous, no account needed)
 *   - sendTestEmail(opts)       sends via chiko@chikochingaya.com GmailProvider (inbox.dog OAuth)
 *   - pollForEmail(addr, token, opts) polls mail.tm until email arrives or times out
 *   - sendAndReceive(opts)      full round-trip: provision -> send -> poll -> return message
 *   - SENDER_EMAIL              'chiko@chikochingaya.com'
 *   - TOKEN_PATH                path to chikochingaya.json credential file
 *   - hasSenderCredentials()    true if chikochingaya.json exists (for skip guards)
 *
 * Usage in a test file:
 *   import { sendAndReceive, hasSenderCredentials } from './helpers/test-mail.ts';
 *
 *   it.skipIf(!hasSenderCredentials())('sends and receives email', async () => {
 *     const { inbox, msg } = await sendAndReceive({ subject: '[test] hello', body: 'world' });
 *     expect(msg?.subject).toContain('[test] hello');
 *   });
 */

import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

// Constants

export const SENDER_EMAIL = 'chiko@chikochingaya.com';
export const TOKEN_PATH = join(homedir(), '.pi/outreach/gmail-accounts/chikochingaya.json');
const CLIENT_ID = 'id_9dd011e46cdf237a6628f39ac1cd35a8';
const MAILTM_BASE = 'https://api.mail.tm';

/** Returns true if the live GmailProvider credentials are available on this machine. */
export function hasSenderCredentials(): boolean {
  return existsSync(TOKEN_PATH);
}

// Types

export interface MailTmInbox {
  /** Unique account ID (used for deletion) */
  id: string;
  /** Full email address for this inbox, e.g. helios-test-xxx@web-library.net */
  address: string;
  /** Password used to create the account */
  password: string;
  /** JWT bearer token for polling /messages */
  token: string;
}

export interface MailTmMessage {
  id: string;
  msgid: string;
  from: { address: string; name: string };
  to: Array<{ address: string; name: string }>;
  subject: string;
  intro: string;
  seen: boolean;
  isDeleted: boolean;
  hasAttachments: boolean;
  size: number;
  downloadUrl: string;
  createdAt: string;
  updatedAt: string;
  accountId: string;
  /** Full text body — only present when fetching /messages/:id */
  text?: string;
  /** HTML body parts — only present when fetching /messages/:id */
  html?: string[];
}

export interface SendTestEmailOpts {
  /** Recipient address */
  to: string;
  /** Email subject line */
  subject: string;
  /** Plain-text email body */
  body: string;
}

export interface PollForEmailOpts {
  /** Only return a message whose subject contains this string (case-insensitive) */
  subjectContains?: string;
  /** Only return a message whose from address contains this string (case-insensitive) */
  fromContains?: string;
  /** Total polling timeout in ms (default: 30000) */
  timeoutMs?: number;
  /** Interval between polls in ms (default: 2000) */
  intervalMs?: number;
}

export interface SendAndReceiveOpts {
  /** Email subject line */
  subject: string;
  /** Plain-text email body */
  body?: string;
  /** Override polling options */
  pollOpts?: PollForEmailOpts;
}

export interface SendAndReceiveResult {
  inbox: MailTmInbox;
  /** The first received message matching the subject, or null if timed out */
  msg: MailTmMessage | null;
}

// provisionInbox

/**
 * Creates a fresh mail.tm inbox using the public REST API (no browser, no account needed).
 *
 * Flow:
 *   1. GET /domains -> pick first active public domain
 *   2. Generate unique address: helios-test-<timestamp>-<random>@<domain>
 *   3. POST /accounts to register the account
 *   4. POST /token to get JWT bearer token
 *   5. Return { id, address, password, token }
 *
 * Throws if mail.tm is rate-limited (429) or rejects the account (422).
 */
export async function provisionInbox(): Promise<MailTmInbox> {
  // Step 1: Get active public domains
  const domainsRes = await fetch(`${MAILTM_BASE}/domains?page=1`);
  if (!domainsRes.ok) {
    throw new Error(`mail.tm GET /domains failed: ${domainsRes.status}`);
  }
  const domainsData = await domainsRes.json() as {
    'hydra:member': Array<{ id: string; domain: string; isActive: boolean; isPrivate: boolean }>;
  };
  const available = domainsData['hydra:member'].filter(d => d.isActive && !d.isPrivate);
  if (!available.length) {
    throw new Error('mail.tm: no active public domains available');
  }
  const domain = available[0].domain;

  // Step 2: Generate unique address
  const unique = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
  const address = `helios-test-${unique}@${domain}`;
  const password = 'Helios2026!';

  // Step 3: Create account (retry with backoff to handle 429 rate limits when tests run in parallel)
  let createRes: Response | undefined;
  for (let attempt = 0; attempt < 3; attempt++) {
    if (attempt > 0) {
      await new Promise<void>(r => setTimeout(r, 3000 * attempt));
    }
    createRes = await fetch(`${MAILTM_BASE}/accounts`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ address, password }),
    });
    if (createRes!.status !== 429) break;
  }

  if (createRes!.status === 429) {
    throw new Error('mail.tm rate limited (429) -- too many account creations (exhausted retries)');
  }
  if (createRes!.status === 422) {
    const err = await createRes!.json() as { 'hydra:description'?: string };
    throw new Error(`mail.tm account creation rejected (422): ${err['hydra:description'] || 'validation error'}`);
  }
  if (!createRes!.ok) {
    throw new Error(`mail.tm POST /accounts failed: ${createRes!.status}`);
  }
  const account = await createRes!.json() as { id: string; address: string };

  // Step 4: Get JWT token
  const tokenRes = await fetch(`${MAILTM_BASE}/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ address, password }),
  });
  if (!tokenRes.ok) {
    throw new Error(`mail.tm POST /token failed: ${tokenRes.status}`);
  }
  const { token } = await tokenRes.json() as { token: string };

  return { id: account.id, address, password, token };
}

// pollForEmail

/**
 * Polls mail.tm /messages every intervalMs until a matching message arrives or timeoutMs elapses.
 *
 * Fetches full message detail (/messages/:id) to include text/html body.
 *
 * @param address   - The inbox address (used only for logging)
 * @param token     - JWT bearer token from provisionInbox()
 * @param opts      - Filtering and timing options
 * @returns The first matched MailTmMessage, or null if timeout
 */
export async function pollForEmail(
  address: string,
  token: string,
  opts: PollForEmailOpts = {}
): Promise<MailTmMessage | null> {
  const timeoutMs = opts.timeoutMs ?? 30_000;
  const intervalMs = opts.intervalMs ?? 2_000;
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    await new Promise(r => setTimeout(r, intervalMs));

    // List messages
    const listRes = await fetch(`${MAILTM_BASE}/messages?page=1`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!listRes.ok) continue;

    const listData = await listRes.json() as { 'hydra:member': MailTmMessage[] };
    const messages = listData['hydra:member'] ?? [];
    if (!messages.length) continue;

    // Apply filters
    let target: MailTmMessage | undefined;
    for (const msg of messages) {
      if (
        opts.subjectContains &&
        !msg.subject?.toLowerCase().includes(opts.subjectContains.toLowerCase())
      ) continue;
      if (
        opts.fromContains &&
        !msg.from?.address?.toLowerCase().includes(opts.fromContains.toLowerCase())
      ) continue;
      target = msg;
      break;
    }
    if (!target) continue;

    // Fetch full message detail (list response has no text/html body)
    const detailRes = await fetch(`${MAILTM_BASE}/messages/${target.id}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!detailRes.ok) continue;

    const full = await detailRes.json() as MailTmMessage;

    // Mark as seen (fire-and-forget)
    fetch(`${MAILTM_BASE}/messages/${target.id}`, {
      method: 'PATCH',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/merge-patch+json',
      },
      body: JSON.stringify({ seen: true }),
    }).catch(() => {});

    return full;
  }

  console.warn(`[test-mail] pollForEmail timed out after ${timeoutMs}ms for ${address}`);
  return null;
}

// sendTestEmail

/**
 * Sends an email via the live chiko@chikochingaya.com GmailProvider (inbox.dog OAuth).
 *
 * Sets HELIOS_EMAIL_ADDRESS and HELIOS_EMAIL_TOKEN_PATH env vars if not already set.
 * Throws if credentials are not available -- check hasSenderCredentials() first.
 *
 * @param opts  - { to, subject, body }
 * @returns     - { id, sent } from GmailProvider.sendEmail()
 */
export async function sendTestEmail(
  opts: SendTestEmailOpts
): Promise<{ id: string; sent: boolean }> {
  if (!hasSenderCredentials()) {
    throw new Error(
      `[test-mail] Sender credentials not found at ${TOKEN_PATH}. ` +
      `Cannot send test email. Guard with hasSenderCredentials() before calling.`
    );
  }

  // Ensure env vars are set (GmailProvider reads them in some code paths)
  if (!process.env.HELIOS_EMAIL_ADDRESS) {
    process.env.HELIOS_EMAIL_ADDRESS = SENDER_EMAIL;
  }
  if (!process.env.HELIOS_EMAIL_TOKEN_PATH) {
    process.env.HELIOS_EMAIL_TOKEN_PATH = TOKEN_PATH;
  }
  if (!process.env.INBOX_DOG_CLIENT_ID) {
    process.env.INBOX_DOG_CLIENT_ID = CLIENT_ID;
  }

  const { GmailProvider } = await import('../../email/providers/gmail.ts');

  const provider = new GmailProvider({
    account: {
      id: 'chikochingaya',
      companyId: 'test-company',
      email: SENDER_EMAIL,
      provider: 'gmail',
      authMethod: 'inbox-dog',
      enabled: true,
      tokenPath: TOKEN_PATH,
    },
  });

  return provider.sendEmail({
    to: [opts.to],
    subject: opts.subject,
    body: opts.body,
  });
}

// sendAndReceive

/**
 * Full round-trip convenience wrapper:
 *   1. Provisions a fresh mail.tm inbox
 *   2. Sends a test email to that inbox via chiko@chikochingaya.com
 *   3. Polls mail.tm until the email arrives (or times out)
 *   4. Returns { inbox, msg }
 *
 * Example:
 *   const { inbox, msg } = await sendAndReceive({ subject: '[test] hello' });
 *   expect(msg?.subject).toContain('[test] hello');
 *
 * @param opts  - { subject, body?, pollOpts? }
 */
export async function sendAndReceive(opts: SendAndReceiveOpts): Promise<SendAndReceiveResult> {
  const subject = opts.subject;
  const body = opts.body ?? `Automated test email from Helios test suite.\nTimestamp: ${new Date().toISOString()}`;

  // 1. Provision inbox
  const inbox = await provisionInbox();

  // 2. Send email
  await sendTestEmail({ to: inbox.address, subject, body });

  // 3. Poll for arrival
  const msg = await pollForEmail(inbox.address, inbox.token, {
    subjectContains: subject,
    timeoutMs: 30_000,
    intervalMs: 2_000,
    ...opts.pollOpts,
  });

  return { inbox, msg };
}
