#!/usr/bin/env node

// =============================================================================
// End-to-End Sandbox Test Script
// =============================================================================
//
// This script walks through the complete application lifecycle in the
// Quantum Sandbox environment:
//
//   1. Submit an application
//   2. Check processing tasks
//   3. Upload a document to trigger offer generation
//   4. Poll until offers are ready (Awaiting Offer Acceptance)
//   5. Retrieve offers
//   6. Download the offer disclosure (required for NY/CA), then accept the offer
//   7. Poll until pending closing info
//   8. Upload a closing document to trigger contract generation
//   9. Poll until documents are sent (Awaiting Document Execution)
//
// Requirements:
//   - Node.js 18 or later (uses built-in fetch)
//   - A valid Sandbox API token set as SANDBOX_API_TOKEN environment variable
//
// Usage:
//   export SANDBOX_API_TOKEN="your-token-here"
//   node e2e-sandbox-test.mjs
//
// =============================================================================

import { readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";

// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------

const BASE_URL = "https://sandbox.quantumlends.com";
const API_TOKEN = process.env.SANDBOX_API_TOKEN;

const POLL_INTERVAL_MS = 4800; // ~5 seconds between status checks
const POLL_TIMEOUT_MS = 180000; // 3 minutes max wait per status transition

if (!API_TOKEN) {
  console.error(
    "Error: SANDBOX_API_TOKEN environment variable is not set.\n" +
      "Set it with: export SANDBOX_API_TOKEN=your-token-here"
  );
  process.exit(1);
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
 * Makes an authenticated API request and returns the parsed JSON response.
 * Throws on non-OK responses with the error body for easy debugging.
 */
async function apiRequest(method, path, body) {
  const url = `${BASE_URL}${path}`;
  const options = {
    method,
    headers: {
      Authorization: `Bearer ${API_TOKEN}`,
    },
  };

  // For JSON requests, set the content type and serialize the body.
  // For FormData (file uploads), let fetch set the content type automatically.
  if (body && !(body instanceof FormData)) {
    options.headers["Content-Type"] = "application/json";
    options.body = JSON.stringify(body);
  } else if (body) {
    options.body = body;
  }

  const response = await fetch(url, options);

  if (!response.ok) {
    const errorBody = await response.text();
    throw new Error(
      `${method} ${path} failed (${response.status}): ${errorBody}`
    );
  }

  // Some responses (like 204) may not have a body
  const text = await response.text();
  return text ? JSON.parse(text) : null;
}

/**
 * Polls the application status endpoint until it reaches the target status.
 * Returns the status response when the target is reached, or throws if the
 * timeout is exceeded.
 */
async function pollStatus(appId, targetStatus) {
  const targets = Array.isArray(targetStatus) ? targetStatus : [targetStatus];
  const startTime = Date.now();

  while (Date.now() - startTime < POLL_TIMEOUT_MS) {
    const status = await apiRequest(
      "GET",
      `/api/v3/applications/${appId}/status`
    );

    if (targets.includes(status.status)) {
      console.log(`  ✓ Status: ${status.status}`);
      return status;
    }

    console.log(`  Status: ${status.status} (waiting for ${targets.join(" or ")}...)`);
    await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
  }

  throw new Error(
    `Timed out waiting for status "${targets.join(" or ")}" after ${POLL_TIMEOUT_MS / 1000}s`
  );
}

/**
 * Uploads a file to the application's documents endpoint.
 * Creates a minimal PDF file with the given filename. The Sandbox uses the
 * filename to determine the document outcome (e.g., "good_doc.pdf" is approved).
 */
async function uploadDocument(appId, filename) {
  // A minimal valid PDF — the Sandbox only cares about the filename, not the content.
  const pdfContent = Buffer.from(
    "%PDF-1.0\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n" +
      "2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n" +
      "3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj\n" +
      "xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n" +
      "0000000058 00000 n \n0000000115 00000 n \n" +
      "trailer<</Size 4/Root 1 0 R>>\nstartxref\n190\n%%EOF"
  );

  const blob = new Blob([pdfContent], { type: "application/pdf" });
  const formData = new FormData();

  // Upload as a bank_statements document type. The Sandbox triggers magic
  // value behavior based on the filename, not the document category.
  formData.append("bank_statements", blob, filename);

  return apiRequest("POST", `/api/v3/applications/${appId}/documents`, formData);
}

// ---------------------------------------------------------------------------
// Application Data
// ---------------------------------------------------------------------------

// This is the application payload we'll submit. It uses two magic values:
//
//   1. doing_business_as: "No-Name"
//      Triggers an incomplete closing document verification task (Business Name
//      Verification). After offer selection, this causes the application to go
//      through the "Pending Closing Information from Customer" status, requiring a document upload
//      to complete the closing process.
//
//   2. Document filename: "good_doc.pdf" (used later during uploads)
//      Triggers automatic document approval in the Sandbox.
//
const application = {
  business: {
    legal_name: "Test Business LLC",
    doing_business_as: "No-Name", // ← Magic value: triggers incomplete closing doc task
    address: {
      street: "123 Main Street",
      city: "Houston",
      // Set state to "NY" or "CA" to exercise the NY/CA disclosure-before-accept
      // flow: offers come back with has_disclosure=true, so Step 6a downloads the
      // disclosure and the offer can then be accepted (accepting first → 412).
      state: "TX",
      zip_code: "77001",
    },
    phone_number: "555-555-5555",
    tax_identification: "12-3456789",
    entity_type: "limited_liability_company",
    naics_code: "541511",
    start_date: "2015-06-15",
  },
  loan_request: {
    amount: 100000,
    purpose: "working_capital",
    authorization: true,
  },
  owners: [
    {
      first_name: "John",
      last_name: "Doe",
      email: "john.doe@testbusiness.com",
      phone_number: "555-555-1234",
      date_of_birth: "1985-03-15",
      is_applicant: true,
      ownership_percent: 100,
      ssn: "123-45-6789",
      address: {
        street: "456 Oak Avenue",
        city: "Houston",
        state: "TX",
        zip_code: "77001",
      },
    },
  ],
  additional_questions: {
    annual_sales: 500000,
  },
};

// ---------------------------------------------------------------------------
// Main Flow
// ---------------------------------------------------------------------------

async function main() {
  console.log("=".repeat(70));
  console.log("Quantum Sandbox — End-to-End Test");
  console.log("=".repeat(70));
  console.log();

  // -------------------------------------------------------------------------
  // Step 1: Submit the application
  // -------------------------------------------------------------------------
  // POST /api/v3/applications/submit creates a new application and returns
  // its ID and initial status. In the Sandbox, applications that don't trigger
  // a decline magic value move to "In Processing" status.
  // -------------------------------------------------------------------------

  console.log("Step 1: Submitting application...");
  const app = await apiRequest("POST", "/api/v3/applications/submit", application);
  const appId = app.id;
  console.log(`  ✓ Application created: ${appId}`);
  console.log(`  ✓ Status: ${app.status}`);
  console.log();

  // -------------------------------------------------------------------------
  // Step 2: Check processing tasks
  // -------------------------------------------------------------------------
  // When an application is In Processing, there are tasks that need to be
  // completed before the application can move forward. In production these
  // are completed by underwriters; in the Sandbox, uploading a document
  // triggers automatic task completion.
  // -------------------------------------------------------------------------

  console.log("Step 2: Waiting for In Processing status, then fetching tasks...");
  await pollStatus(appId, "In Processing");

  // Brief pause to allow tasks to be created after status transition
  await new Promise((r) => setTimeout(r, 3000));

  const tasks = await apiRequest(
    "GET",
    `/api/v3/applications/${appId}/tasks?show_details=true`
  );
  console.log(`  ✓ Found ${tasks.length} task(s):`);
  for (const task of tasks) {
    console.log(`    - [${task.status}] ${task.title}`);
  }
  console.log();

  // -------------------------------------------------------------------------
  // Step 3: Upload a document to trigger offer generation
  // -------------------------------------------------------------------------
  // Uploading a document named "good_doc.pdf" while the application is
  // In Processing does two things:
  //   1. The document is automatically approved (magic filename)
  //   2. The offer generation pipeline is triggered, which will move the
  //      application through Under Credit Review → Approved → Awaiting Offer Acceptance
  // -------------------------------------------------------------------------

  console.log("Step 3: Uploading document to trigger offer generation...");
  const uploadResult = await uploadDocument(appId, "good_doc.pdf");
  console.log(
    `  ✓ Document uploaded (${uploadResult.documents?.length || 0} document(s))`
  );
  console.log();

  // -------------------------------------------------------------------------
  // Step 4: Poll until status reaches "Awaiting Offer Acceptance"
  // -------------------------------------------------------------------------
  // The offer generation pipeline runs asynchronously. The application will
  // transition through these statuses:
  //   In Processing → Under Credit Review → Approved → Awaiting Offer Acceptance
  //
  // We poll the status endpoint every 4 seconds until we see
  // "Awaiting Offer Acceptance".
  // -------------------------------------------------------------------------

  console.log("Step 4: Waiting for offers to be generated...");
  await pollStatus(appId, "Awaiting Offer Acceptance");
  console.log();

  // -------------------------------------------------------------------------
  // Step 5: Retrieve offers
  // -------------------------------------------------------------------------
  // Once the status is "Awaiting Offer Acceptance", we can retrieve the available loan offers.
  // The default Sandbox configuration generates one Term Loan and one Fee Based
  // Line of Credit.
  // -------------------------------------------------------------------------

  console.log("Step 5: Retrieving offers...");
  const offersResponse = await apiRequest(
    "GET",
    `/api/v3/applications/${appId}/offers`
  );
  const offers = offersResponse.offers;
  console.log(`  ✓ Found ${offers.length} offer(s):`);
  for (const offer of offers) {
    console.log(
      `    - ${offer.loan_type} | ` +
        `${((offer.amount_in_cents || offer.facility_size_in_cents) / 100).toLocaleString("en-US", { style: "currency", currency: "USD" })} | ` +
        `${offer.term} months | ` +
        `ID: ${offer.id}`
    );
  }
  console.log();

  // -------------------------------------------------------------------------
  // Step 6: Download the disclosure if present (6a), then accept the offer (6b)
  // -------------------------------------------------------------------------

  const selectedOffer = offers[0];

  // Download the offer disclosure before accepting. The offers response exposes
  // `has_disclosure` (true for NY/CA businesses, where a disclosure exists and
  // MUST be downloaded before acceptance — accepting first returns 412). Only
  // call the disclosure endpoint when a disclosure is available.
  if (selectedOffer.has_disclosure) {
    console.log(`Step 6a: Downloading disclosure for offer: ${selectedOffer.id}...`);
    const disclosureResponse = await fetch(
      `${BASE_URL}/api/v3/applications/${appId}/offers/${selectedOffer.id}/disclosure`,
      { headers: { Authorization: `Bearer ${API_TOKEN}` } }
    );
    if (!disclosureResponse.ok) {
      throw new Error(
        `Disclosure download failed (${disclosureResponse.status})`
      );
    }
    // Drain the response body so the PDF is actually downloaded (and the
    // connection can be reused). Present/retain the bytes as needed.
    const disclosurePdf = await disclosureResponse.arrayBuffer();
    console.log(`  ✓ Disclosure downloaded (${disclosurePdf.byteLength} bytes)`);
  }

  console.log(`Step 6b: Accepting offer: ${selectedOffer.id} (${selectedOffer.loan_type})...`);
  await apiRequest(
    "POST",
    `/api/v3/applications/${appId}/offer/${selectedOffer.id}/accept`
  );
  console.log("  ✓ Offer accepted");
  console.log();

  // -------------------------------------------------------------------------
  // Step 7: Poll until status reaches "Pending Closing Information from Customer"
  // -------------------------------------------------------------------------
  // Because we used the "No-Name" magic value for doing_business_as, there is
  // an incomplete closing document verification task (Business Name Verification).
  // This means the application follows "Flow A":
  //   Awaiting Offer Acceptance → Preparing Loan Documents → Pending Closing Information from Customer
  //
  // The application will wait in "Pending Closing Information from Customer"
  // until we upload a document to complete the remaining verification tasks.
  // -------------------------------------------------------------------------

  console.log("Step 7: Waiting for Pending Closing Information from Customer status...");
  await pollStatus(appId, "Pending Closing Information from Customer");
  console.log();

  // -------------------------------------------------------------------------
  // Step 8: Upload a closing document to trigger contract generation
  // -------------------------------------------------------------------------
  // Uploading "good_doc.pdf" while in "Pending Closing Information from Customer" status:
  //   1. Completes the remaining closing document verification tasks
  //   2. Generates the loan contract
  //   3. Moves the application to "Awaiting Document Execution"
  // -------------------------------------------------------------------------

  console.log("Step 8: Uploading closing document...");
  const closingUpload = await uploadDocument(appId, "good_doc.pdf");
  console.log(
    `  ✓ Document uploaded (${closingUpload.documents?.length || 0} document(s))`
  );
  console.log();

  // -------------------------------------------------------------------------
  // Step 9: Poll until status reaches "Awaiting Document Execution"
  // -------------------------------------------------------------------------
  // The closing pipeline runs asynchronously. The application will transition:
  //   Pending Closing Information from Customer → Awaiting Document Execution
  //
  // "Awaiting Document Execution" means the loan contract has been generated
  // and sent to the borrower for signature.
  // -------------------------------------------------------------------------

  console.log("Step 9: Waiting for Awaiting Document Execution status...");
  await pollStatus(appId, "Awaiting Document Execution");
  console.log();

  // -------------------------------------------------------------------------
  // Done!
  // -------------------------------------------------------------------------

  console.log("=".repeat(70));
  console.log("End-to-end test complete!");
  console.log();
  console.log(`  Application ID: ${appId}`);
  console.log("  Final Status:   Awaiting Document Execution");
  console.log();
  console.log("  The loan contract has been generated and sent to the borrower.");
  console.log("=".repeat(70));
}

// Run the script
main().catch((error) => {
  console.error("\n✗ Test failed:", error.message);
  process.exit(1);
});
