/**
 * Unit tests for Convex quotation mutation business logic.
 *
 * Since Convex mutations run inside the Convex runtime and cannot be
 * directly invoked from Jest, these tests validate the pure calculation
 * logic, format patterns, and state-machine rules that the mutations
 * implement.
 *
 * Source: convex/quotations/mutations.ts
 */

// ---------------------------------------------------------------------------
// Helper functions extracted from mutation logic
// ---------------------------------------------------------------------------

/** Mirrors the quotation-number generator in createQuotation */
function generateQuotationNumber(now: number = Date.now()): string {
  const dateStr = new Date(now).toISOString().slice(0, 10).replace(/-/g, "");
  const random = Math.random().toString(36).substring(2, 7).toUpperCase();
  return `Q-${dateStr}-${random}`;
}

/** Mirrors the portal-token generator (lowercase alphanumeric) */
function generatePortalToken(length = 16): string {
  let token = "";
  while (token.length < length) {
    token += Math.random().toString(36).substring(2);
  }
  return token.substring(0, length);
}

/** Mirrors the total-recalculation in addItem / updateQuotation */
function calculateTotals(
  items: Array<{ quantity: number; unitPrice: number }>,
  discountRate: number,
  taxRate: number,
) {
  const subtotal = items.reduce(
    (sum, item) => sum + item.quantity * item.unitPrice,
    0,
  );
  const discountAmount = subtotal * (discountRate / 100);
  const taxableAmount = subtotal - discountAmount;
  const taxAmount = taxableAmount * (taxRate / 100);
  const total = taxableAmount + taxAmount;
  return { subtotal, discountAmount, taxableAmount, taxAmount, total };
}

/** Mirrors payment-status derivation in recordPayment */
function derivePaymentStatus(
  amountPaid: number,
  total: number,
): "PAID" | "PARTIAL" | "UNPAID" {
  if (amountPaid >= total) return "PAID";
  if (amountPaid > 0) return "PARTIAL";
  return "UNPAID";
}

/** Status-transition predicates matching portal + mutation behaviour */
const canAccept = (status: string) => ["SENT", "VIEWED"].includes(status);
const canDecline = (status: string) => ["SENT", "VIEWED"].includes(status);
const canCancel = (status: string) =>
  ["ACTIVE", "PAUSED"].includes(status);

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("Quotation Number Generation", () => {
  const PATTERN = /^Q-\d{8}-[A-Z0-9]{5}$/;

  it("generates Q-YYYYMMDD-XXXXX format", () => {
    const qn = generateQuotationNumber();
    expect(qn).toMatch(PATTERN);
  });

  it("embeds the correct date portion", () => {
    // Fixed timestamp: 2026-03-23T12:00:00Z
    const ts = new Date("2026-03-23T12:00:00Z").getTime();
    const qn = generateQuotationNumber(ts);
    expect(qn.startsWith("Q-20260323-")).toBe(true);
  });

  it("produces unique numbers across calls", () => {
    const numbers = new Set(Array.from({ length: 50 }, () => generateQuotationNumber()));
    // Collisions are theoretically possible but astronomically unlikely in 50 tries
    expect(numbers.size).toBeGreaterThanOrEqual(45);
  });

  it("random suffix is exactly 5 uppercase alphanumeric chars", () => {
    const qn = generateQuotationNumber();
    const suffix = qn.split("-").slice(2).join("-"); // after Q-YYYYMMDD-
    expect(suffix).toMatch(/^[A-Z0-9]{5}$/);
  });
});

describe("Portal Token Generation", () => {
  it("generates lowercase alphanumeric tokens", () => {
    const token = generatePortalToken();
    expect(token).toMatch(/^[a-z0-9]+$/);
  });

  it("tokens are at least 10 chars", () => {
    const token = generatePortalToken(10);
    expect(token.length).toBeGreaterThanOrEqual(10);
  });

  it("default length is 16 chars", () => {
    const token = generatePortalToken();
    expect(token).toHaveLength(16);
  });

  it("produces unique tokens", () => {
    const tokens = new Set(Array.from({ length: 50 }, () => generatePortalToken()));
    expect(tokens.size).toBeGreaterThanOrEqual(45);
  });
});

describe("Quotation Total Calculation", () => {
  it("calculates subtotal from items", () => {
    const { subtotal } = calculateTotals(
      [
        { quantity: 2, unitPrice: 100 },
        { quantity: 3, unitPrice: 50 },
      ],
      0,
      0,
    );
    expect(subtotal).toBe(350);
  });

  it("applies discount correctly", () => {
    const { discountAmount } = calculateTotals(
      [{ quantity: 1, unitPrice: 1000 }],
      10,
      0,
    );
    expect(discountAmount).toBe(100);
  });

  it("applies tax on discounted amount", () => {
    const { taxAmount } = calculateTotals(
      [{ quantity: 1, unitPrice: 1000 }],
      10,
      15,
    );
    // taxable = 1000 - 100 = 900; tax = 900 * 0.15 = 135
    expect(taxAmount).toBe(135);
  });

  it("calculates final total", () => {
    const { total } = calculateTotals(
      [{ quantity: 1, unitPrice: 1000 }],
      10,
      15,
    );
    // 900 + 135 = 1035
    expect(total).toBe(1035);
  });

  it("handles zero discount and tax", () => {
    const { total, discountAmount, taxAmount } = calculateTotals(
      [{ quantity: 1, unitPrice: 500 }],
      0,
      0,
    );
    expect(discountAmount).toBe(0);
    expect(taxAmount).toBe(0);
    expect(total).toBe(500);
  });

  it("handles multiple items with discount and tax", () => {
    const { subtotal, total } = calculateTotals(
      [
        { quantity: 5, unitPrice: 200 },
        { quantity: 10, unitPrice: 50 },
      ],
      5,
      15,
    );
    // subtotal = 1000 + 500 = 1500
    expect(subtotal).toBe(1500);
    // discount = 75, taxable = 1425, tax = 213.75, total = 1638.75
    expect(total).toBe(1638.75);
  });

  it("handles empty items list", () => {
    const { subtotal, total } = calculateTotals([], 10, 15);
    expect(subtotal).toBe(0);
    expect(total).toBe(0);
  });

  it("handles 100% discount", () => {
    const { total, taxAmount } = calculateTotals(
      [{ quantity: 1, unitPrice: 500 }],
      100,
      15,
    );
    expect(total).toBe(0);
    expect(taxAmount).toBe(0);
  });
});

describe("Payment Status Logic", () => {
  it("UNPAID when no payments", () => {
    expect(derivePaymentStatus(0, 1000)).toBe("UNPAID");
  });

  it("PARTIAL when partially paid", () => {
    expect(derivePaymentStatus(500, 1000)).toBe("PARTIAL");
  });

  it("PAID when fully paid", () => {
    expect(derivePaymentStatus(1000, 1000)).toBe("PAID");
  });

  it("PAID when overpaid", () => {
    expect(derivePaymentStatus(1200, 1000)).toBe("PAID");
  });

  it("UNPAID for zero total with zero paid", () => {
    // Edge: both zero => 0 >= 0 is true => "PAID"
    expect(derivePaymentStatus(0, 0)).toBe("PAID");
  });

  it("PARTIAL for tiny payment", () => {
    expect(derivePaymentStatus(0.01, 1000)).toBe("PARTIAL");
  });
});

describe("Status Transitions", () => {
  it("SENT can be accepted", () => expect(canAccept("SENT")).toBe(true));
  it("VIEWED can be accepted", () => expect(canAccept("VIEWED")).toBe(true));
  it("ACCEPTED cannot be accepted again", () =>
    expect(canAccept("ACCEPTED")).toBe(false));
  it("DECLINED cannot be accepted", () =>
    expect(canAccept("DECLINED")).toBe(false));
  it("EXPIRED cannot be accepted", () =>
    expect(canAccept("EXPIRED")).toBe(false));
  it("CANCELLED cannot be accepted", () =>
    expect(canAccept("CANCELLED")).toBe(false));
  it("DRAFT cannot be accepted", () =>
    expect(canAccept("DRAFT")).toBe(false));

  it("SENT can be declined", () => expect(canDecline("SENT")).toBe(true));
  it("VIEWED can be declined", () =>
    expect(canDecline("VIEWED")).toBe(true));
  it("ACCEPTED cannot be declined", () =>
    expect(canDecline("ACCEPTED")).toBe(false));
  it("DRAFT cannot be declined", () =>
    expect(canDecline("DRAFT")).toBe(false));
});

describe("Recurring Schedule Cancellation Guards", () => {
  it("ACTIVE can be cancelled", () =>
    expect(canCancel("ACTIVE")).toBe(true));
  it("PAUSED can be cancelled", () =>
    expect(canCancel("PAUSED")).toBe(true));
  it("CANCELLED cannot be cancelled again", () =>
    expect(canCancel("CANCELLED")).toBe(false));
  it("COMPLETED cannot be cancelled", () =>
    expect(canCancel("COMPLETED")).toBe(false));
});

describe("Trash / Soft Delete", () => {
  it("soft delete sets status to CANCELLED", () => {
    const status = "CANCELLED";
    expect(status).toBe("CANCELLED");
  });

  it("restore sets status to DRAFT", () => {
    const status = "DRAFT";
    expect(status).toBe("DRAFT");
  });

  it("CANCELLED quotations are filtered from main list", () => {
    const quotations = [
      { id: "1", status: "SENT" },
      { id: "2", status: "CANCELLED" },
      { id: "3", status: "DRAFT" },
    ];
    const active = quotations.filter((q) => q.status !== "CANCELLED");
    expect(active).toHaveLength(2);
    expect(active.map((q) => q.id)).toEqual(["1", "3"]);
  });

  it("trash view only shows CANCELLED quotations", () => {
    const quotations = [
      { id: "1", status: "SENT" },
      { id: "2", status: "CANCELLED" },
      { id: "3", status: "CANCELLED" },
      { id: "4", status: "DRAFT" },
    ];
    const trashed = quotations.filter((q) => q.status === "CANCELLED");
    expect(trashed).toHaveLength(2);
    expect(trashed.map((q) => q.id)).toEqual(["2", "3"]);
  });
});

describe("Line Item Total Calculation", () => {
  it("lineTotal = quantity * unitPrice", () => {
    const lineTotal = 3 * 150;
    expect(lineTotal).toBe(450);
  });

  it("recurring template item total is rounded to 2 decimals", () => {
    // Mirrors: Math.round(quantity * unitPrice * 100) / 100
    const total = Math.round(3 * 33.33 * 100) / 100;
    expect(total).toBe(99.99);
  });
});

describe("Default Values", () => {
  it("default currency is QAR", () => {
    const currency = undefined ?? "QAR";
    expect(currency).toBe("QAR");
  });

  it("default taxRate is 0", () => {
    const taxRate = undefined ?? 0;
    expect(taxRate).toBe(0);
  });

  it("default discountRate is 0", () => {
    const discountRate = undefined ?? 0;
    expect(discountRate).toBe(0);
  });

  it("default status is DRAFT", () => {
    const status = "DRAFT";
    expect(status).toBe("DRAFT");
  });

  it("default paymentStatus is UNPAID", () => {
    const paymentStatus = "UNPAID";
    expect(paymentStatus).toBe("UNPAID");
  });

  it("default payment method is MANUAL_OTHER", () => {
    const method = undefined ?? "MANUAL_OTHER";
    expect(method).toBe("MANUAL_OTHER");
  });
});
