/**
 * Test Script: Forecast Item Resequencing & Procurement Linkage
 *
 * Validates the CW forecast API behaviour discovered via probing:
 *   - `sequenceNumber` is read-only — display order = array position
 *   - PUT always regenerates all forecast item IDs
 *   - Revenue & cost are preserved through PUT
 *   - PATCH on /forecast with `/forecastItems/{idx}/field` paths works
 *     for some fields (e.g. forecastDescription) and preserves IDs
 *
 * Test flow:
 *   1. Create opportunity under XYZ Test Company
 *   2. Add 4 products via POST
 *   3. Create procurement products (linked by forecastDetailId)
 *   4. Cancel one procurement product
 *   5. Reorder forecast items via PUT (reverse order)
 *   6. Remap procurement forecastDetailId to new IDs
 *   7. Verify: order correct, prices preserved, cancellation data intact
 *   8. Clean up
 *
 * Usage:  bun run test-forecast-resequence.ts
 */
import axios from "axios";

const cw = axios.create({
  baseURL: "https://ttscw.totaltech.net/v4_6_release/apis/3.0/",
  headers: {
    Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
    clientId: `${process.env.CW_CLIENT_ID}`,
    "Content-Type": "application/json",
  },
});

const log = (label: string, ...args: unknown[]) =>
  console.log(`\n[${label}]`, ...args);

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function main() {
  // ── 1. Find company ─────────────────────────────────────────────────────
  log("SETUP", "Finding XYZ Test Company...");
  const compRes = await cw.get(
    `/company/companies?conditions=${encodeURIComponent("name like 'XYZ Test%'")}&fields=id,identifier,name`,
  );
  if (compRes.data.length === 0) {
    console.error("ERROR: 'XYZ Test Company' not found.");
    process.exit(1);
  }
  const company = compRes.data[0];
  log("SETUP", `Company: ${company.name} (id=${company.id})`);

  // ── 2. Create opportunity ───────────────────────────────────────────────
  log("SETUP", "Creating test opportunity...");
  const oppRes = await cw.post("/sales/opportunities", {
    name: `[TEST] Resequence – ${new Date().toISOString().slice(0, 16)}`,
    company: { id: company.id },
    contact: { id: 1 },
    primarySalesRep: { id: 153 },
    expectedCloseDate: new Date(Date.now() + 30 * 86_400_000)
      .toISOString()
      .replace(/\.\d{3}Z$/, "Z"),
  });
  const oppId = oppRes.data.id;
  log("SETUP", `Created opportunity id=${oppId}`);

  const forecastUrl = `/sales/opportunities/${oppId}/forecast`;

  // Track IDs for cleanup
  const procIdsToClean: number[] = [];

  try {
    // ── 3. Add 4 products ───────────────────────────────────────────────────
    log("PRODUCTS", "Adding 4 products...");
    const postRes = await cw.post(forecastUrl, {
      forecastItems: [
        {
          opportunity: { id: oppId },
          status: { id: 1 },
          forecastDescription: "Alpha",
          revenue: 100,
          cost: 50,
          forecastType: "Product",
        },
        {
          opportunity: { id: oppId },
          status: { id: 1 },
          forecastDescription: "Bravo",
          revenue: 250,
          cost: 125,
          forecastType: "Product",
        },
        {
          opportunity: { id: oppId },
          status: { id: 1 },
          forecastDescription: "Charlie",
          revenue: 30,
          cost: 10,
          forecastType: "Product",
        },
        {
          opportunity: { id: oppId },
          status: { id: 1 },
          forecastDescription: "Delta",
          revenue: 75,
          cost: 40,
          forecastType: "Product",
        },
      ],
    });
    const items: any[] = postRes.data.forecastItems ?? [];
    log("PRODUCTS", `Created ${items.length} items:`);
    for (const it of items) {
      console.log(
        `  id=${it.id} desc="${it.forecastDescription}" rev=${it.revenue} cost=${it.cost}`,
      );
    }

    // Snapshot prices
    const priceSnap = new Map<string, { rev: number; cost: number }>(
      items.map((i) => [
        i.forecastDescription,
        { rev: i.revenue, cost: i.cost },
      ]),
    );

    // ── 4. Create procurement products ──────────────────────────────────────
    log("PROCUREMENT", "Creating procurement products...");
    const procProducts: any[] = [];
    for (const item of items) {
      try {
        const pr = await cw.post("/procurement/products", {
          catalogItem: { id: 87 },
          description: item.forecastDescription,
          quantity: 1,
          price: item.revenue,
          cost: item.cost,
          billableOption: "Billable",
          opportunity: { id: oppId },
          forecastDetailId: item.id,
        });
        procProducts.push(pr.data);
        procIdsToClean.push(pr.data.id);
        console.log(
          `  ✓ Proc ${pr.data.id} → forecastDetailId=${pr.data.forecastDetailId} "${item.forecastDescription}"`,
        );
      } catch (e: any) {
        console.log(
          `  ✗ Failed: ${e.response?.status} ${JSON.stringify(e.response?.data)}`,
        );
      }
    }

    if (procProducts.length === 0) {
      log(
        "PROCUREMENT",
        "Could not create procurement products (permission issue?).",
      );
      log(
        "PROCUREMENT",
        "Will run reorder test without cancellation verification.",
      );
    }

    // ── 5. Cancel "Bravo" procurement product ───────────────────────────────
    const bravoProc = procProducts.find((p: any) => p.description === "Bravo");
    if (bravoProc) {
      log("CANCEL", `Cancelling Bravo (proc id=${bravoProc.id})...`);
      try {
        await cw.patch(`/procurement/products/${bravoProc.id}`, [
          { op: "replace", path: "cancelledFlag", value: true },
          { op: "replace", path: "quantityCancelled", value: 1 },
          {
            op: "replace",
            path: "cancelledReason",
            value: "Test cancellation",
          },
        ]);
        log("CANCEL", "✓ Cancelled.");
      } catch (e: any) {
        log(
          "CANCEL",
          `✗ ${e.response?.status} ${JSON.stringify(e.response?.data)}`,
        );
      }
    }

    // ── 5b. Check for auto-created forecast items ─────────────────────────
    await sleep(300);
    const midForecast = await cw.get(forecastUrl);
    const midItems = midForecast.data.forecastItems ?? [];
    log(
      "OBSERVE",
      `Forecast items after procurement creation: ${midItems.length} (was ${items.length})`,
    );
    if (midItems.length !== items.length) {
      log(
        "OBSERVE",
        "⚠ Creating procurement products auto-created additional forecast items!",
      );
      for (const mi of midItems) {
        const isOriginal = items.some((i: any) => i.id === mi.id);
        console.log(
          `  id=${mi.id} desc="${mi.forecastDescription}" ${isOriginal ? "(original)" : "(AUTO-CREATED by procurement)"}`,
        );
      }
    }

    // Snapshot procurement state before reorder
    const beforeProc = await cw.get(
      `/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,description`,
    );
    // Build map by description for cross-PUT comparison (IDs will change)
    const beforeByDesc = new Map<string, any>();
    log(
      "SNAPSHOT",
      `${beforeProc.data.length} procurement products before reorder:`,
    );
    for (const p of beforeProc.data) {
      beforeByDesc.set(p.description, p);
      console.log(
        `  Proc ${p.id}: forecastDetailId=${p.forecastDetailId} cancelled=${p.cancelledFlag} qty=${p.quantityCancelled} reason="${p.cancelledReason ?? ""}" "${p.description}"`,
      );
    }

    // Record old procurement IDs for later comparison
    const oldProcIds = new Set(beforeProc.data.map((p: any) => p.id));

    // ── 6. Reorder: reverse ONLY the original 4 forecast items ──────────────
    log("REORDER", "Reversing forecast item order via PUT...");

    // Only reorder the original items; keep any auto-created ones in place
    const originalDescs = new Set(items.map((i: any) => i.forecastDescription));
    const originals = midItems.filter(
      (i: any) =>
        originalDescs.has(i.forecastDescription) &&
        items.some((o: any) => o.id === i.id),
    );
    const extras = midItems.filter(
      (i: any) => !originals.some((o: any) => o.id === i.id),
    );

    const reversedOriginals = [...originals].reverse();
    const reorderedAll = [...reversedOriginals, ...extras];

    const clone = JSON.parse(JSON.stringify(midForecast.data));
    clone.forecastItems = JSON.parse(JSON.stringify(reorderedAll));

    const putRes = await cw.put(forecastUrl, clone);
    const newItems: any[] = putRes.data.forecastItems ?? [];

    log("REORDER", `After PUT (${newItems.length} items):`);
    for (const it of newItems) {
      console.log(
        `  id=${it.id} desc="${it.forecastDescription}" rev=${it.revenue} cost=${it.cost}`,
      );
    }

    // Build old→new ID map by position (for original items only)
    const idMap = new Map<number, number>();
    for (let i = 0; i < reversedOriginals.length && i < newItems.length; i++) {
      idMap.set(reversedOriginals[i].id, newItems[i].id);
    }
    log("ID MAP", "Forecast item Old → New:");
    for (const [oldId, newId] of idMap) {
      console.log(`  ${oldId} → ${newId}`);
    }

    // ── 7. Check if procurement products survived PUT ───────────────────────
    await sleep(300);
    const afterProc = await cw.get(
      `/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,description`,
    );
    const newProcIds = new Set(afterProc.data.map((p: any) => p.id));

    log(
      "PROCUREMENT SURVIVAL",
      "Checking if procurement product IDs survived PUT...",
    );
    const procSurvived = [...oldProcIds].every((id) => newProcIds.has(id));
    if (procSurvived) {
      console.log("  ✓ All original procurement product IDs survived PUT.");
    } else {
      console.log("  ✗ PUT REGENERATED procurement product IDs!");
      console.log(`    Before: [${[...oldProcIds].join(", ")}]`);
      console.log(`    After:  [${[...newProcIds].join(", ")}]`);
    }

    // Try remap if old IDs still exist
    let remapOk = true;
    if (procSurvived) {
      log("REMAP", "Updating procurement products forecastDetailId...");
      for (const pp of beforeProc.data) {
        const oldFdId = pp.forecastDetailId as number;
        const newFdId = idMap.get(oldFdId);
        if (!newFdId || newFdId === oldFdId) continue;
        try {
          await cw.patch(`/procurement/products/${pp.id}`, [
            { op: "replace", path: "forecastDetailId", value: newFdId },
          ]);
          console.log(
            `  ✓ Proc ${pp.id}: forecastDetailId ${oldFdId} → ${newFdId}`,
          );
        } catch (e: any) {
          remapOk = false;
          console.log(
            `  ✗ Proc ${pp.id} remap failed: ${e.response?.status} ${JSON.stringify(e.response?.data)}`,
          );
        }
      }
    } else {
      remapOk = false;
      log(
        "REMAP",
        "⚠ SKIPPED — procurement products were regenerated by PUT; old IDs no longer exist.",
      );
    }

    // ── 8. Verify ───────────────────────────────────────────────────────────
    await sleep(300);

    // 8a. Verify order (first 4 items)
    log("VERIFY ORDER", "Expected reverse: Delta, Charlie, Bravo, Alpha");
    const expectedOrder = ["Delta", "Charlie", "Bravo", "Alpha"];
    let orderOk = true;
    for (let i = 0; i < expectedOrder.length; i++) {
      const actual = newItems[i]?.forecastDescription;
      const ok = actual === expectedOrder[i];
      if (!ok) orderOk = false;
      console.log(
        `  Position ${i}: ${ok ? "✓" : "✗"} expected "${expectedOrder[i]}", got "${actual}"`,
      );
    }

    // 8b. Verify prices (by description)
    log("VERIFY PRICES", "");
    let pricesOk = true;
    for (const item of newItems) {
      const orig = priceSnap.get(item.forecastDescription);
      if (!orig) continue;
      if (item.revenue !== orig.rev || item.cost !== orig.cost) {
        pricesOk = false;
        console.log(
          `  ✗ "${item.forecastDescription}": rev ${orig.rev}→${item.revenue}, cost ${orig.cost}→${item.cost}`,
        );
      }
    }
    if (pricesOk) console.log("  ✓ All prices preserved.");

    // 8c. Verify cancellation data — match by description since IDs may have changed
    let cancelOk = true;
    if (procProducts.length > 0) {
      log(
        "VERIFY CANCELLATION",
        "Checking cancellation data on procurement products after PUT...",
      );
      const finalProc = await cw.get(
        `/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,description`,
      );

      // Track by procIdsToClean for cleanup
      for (const p of finalProc.data) {
        if (!procIdsToClean.includes(p.id)) procIdsToClean.push(p.id);
      }

      for (const pp of finalProc.data) {
        const orig = beforeByDesc.get(pp.description);
        if (!orig) {
          console.log(
            `  ? Proc ${pp.id} "${pp.description}" — no matching pre-PUT record`,
          );
          continue;
        }

        const cancelledMatch =
          pp.cancelledFlag === orig.cancelledFlag &&
          pp.quantityCancelled === orig.quantityCancelled &&
          (pp.cancelledReason ?? "") === (orig.cancelledReason ?? "");

        if (!cancelledMatch) {
          cancelOk = false;
          console.log(
            `  ✗ Proc ${pp.id} "${pp.description}": CANCELLATION DATA CHANGED\n` +
              `    Before: cancelled=${orig.cancelledFlag} qty=${orig.quantityCancelled} reason="${orig.cancelledReason ?? ""}"\n` +
              `    After:  cancelled=${pp.cancelledFlag} qty=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
          );
        } else {
          console.log(
            `  ✓ Proc ${pp.id} "${pp.description}": cancelled=${pp.cancelledFlag} qty=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
          );
        }
      }
    }

    // ── Summary ─────────────────────────────────────────────────────────────
    log("SUMMARY", "");
    console.log(
      "  Order correct:              ",
      orderOk ? "✓ PASS" : "✗ FAIL",
    );
    console.log(
      "  Prices preserved:           ",
      pricesOk ? "✓ PASS" : "✗ FAIL",
    );
    console.log(
      "  Proc IDs survived PUT:      ",
      procSurvived ? "✓ PASS" : "✗ FAIL",
    );
    console.log(
      "  Procurement remap:          ",
      remapOk ? "✓ PASS" : "✗ FAIL (skipped or failed)",
    );
    console.log(
      "  Cancellation data preserved:",
      cancelOk ? "✓ PASS" : "✗ FAIL",
    );

    const allPass = orderOk && pricesOk && procSurvived && remapOk && cancelOk;
    log("RESULT", allPass ? "✓ ALL TESTS PASSED" : "✗ SOME TESTS FAILED");
  } finally {
    // ── Cleanup ─────────────────────────────────────────────────────────────
    log("CLEANUP", "Deleting procurement products...");
    for (const id of procIdsToClean) {
      try {
        await cw.delete(`/procurement/products/${id}`);
      } catch {}
    }
    log("CLEANUP", `Deleted ${procIdsToClean.length} procurement products.`);

    log("CLEANUP", `Deleting opportunity ${oppId}...`);
    try {
      await cw.delete(`/sales/opportunities/${oppId}`);
      log("CLEANUP", "✓ Done.");
    } catch (e: any) {
      log("CLEANUP", `✗ ${e.response?.status ?? e.message}`);
    }
  }
}

main().catch((err) => {
  console.error("\n[FATAL]", err.response?.data ?? err.message);
  process.exit(1);
});
