import { describe, it, expect, afterEach } from "vitest";
import prisma from "../config/database";
import { createProject } from "../services/projects.service";
import {
  createOrder,
  createOrderFromQuotation,
  deleteOrder,
} from "../services/orders.service";
import { previewProjectNumber } from "../services/numbering.service";

const YEAR = new Date().getFullYear();

const createdProjectIds: number[] = [];
const createdOrderIds: number[] = [];
const createdQuotationIds: number[] = [];

afterEach(async () => {
  for (const id of createdProjectIds)
    await prisma.projects.deleteMany({ where: { id } });
  for (const id of createdOrderIds)
    await prisma.orders.deleteMany({ where: { id } });
  for (const id of createdQuotationIds)
    await prisma.quotations.deleteMany({ where: { id } });
  createdProjectIds.length = 0;
  createdOrderIds.length = 0;
  createdQuotationIds.length = 0;
  // Orders and projects now draw from SEPARATE sequences — reset both.
  await prisma.number_sequences.deleteMany({
    where: { type: { in: ["shared", "project"] } },
  });
});

/**
 * Read the current `last_number` of a (type, current-year) sequence row, or 0
 * when the row doesn't exist yet (the first allocation creates it at 1). Used to
 * prove each generator advances its OWN row by exactly 1.
 */
async function seqLastNumber(type: "shared" | "project"): Promise<number> {
  const row = await prisma.number_sequences.findFirst({
    where: { type, year: YEAR },
    select: { last_number: true },
  });
  return row?.last_number ?? 0;
}

const baseOrder = {
  status: "prijata" as const,
  currency: "CZK",
  language: "cs",
  exchange_rate: 1,
};

/**
 * Fail loudly (instead of the old `if (!("id" in res)) return;`, which silently
 * passed the test when a create failed) while still narrowing the union via the
 * `in` operator so the success-branch fields stay typed.
 */
function failResult(res: unknown): never {
  throw new Error(`expected a success result, got ${JSON.stringify(res)}`);
}

describe("createOrder (manual, optional linked project)", () => {
  it("creates an order AND a project, each with its OWN number, linked by FK", async () => {
    // Capture BOTH sequence counters before the create so we can prove each
    // generator advanced its OWN row (not that the project consumed the shared
    // counter). Format difference alone (OBJ-… vs 26730…) would let `not.toBe`
    // pass even if the project drew from `shared` — the deltas catch that.
    const sharedBefore = await seqLastNumber("shared");
    const projectBefore = await seqLastNumber("project");

    const res = await createOrder({
      ...baseOrder,
      scope_title: "Ruční zakázka",
      create_project: true,
    });
    if (!("id" in res)) failResult(res);
    createdOrderIds.push(res.id!);
    expect(res.project_id).toBeTruthy();
    const project = await prisma.projects.findUnique({
      where: { id: res.project_id! },
    });
    if (project) createdProjectIds.push(project.id);
    // The auto-created project now draws from the dedicated project sequence,
    // so its number is distinct from the order's shared number. They relate
    // only via the order_id FK, not by sharing a number.
    expect(project?.project_number).toBeTruthy();
    expect(project?.project_number).not.toBe(res.order_number);
    expect(project?.order_id).toBe(res.id);

    // Sequence isolation: the order consumed exactly one `shared` number and
    // the project consumed exactly one `project` number — each generator
    // advanced its OWN row by 1. A regression where the project draws from the
    // `shared` counter would bump `shared` by 2 and leave `project` at 0.
    const sharedAfter = await seqLastNumber("shared");
    const projectAfter = await seqLastNumber("project");
    expect(sharedAfter - sharedBefore).toBe(1);
    expect(projectAfter - projectBefore).toBe(1);
  });

  it("creates only the order when create_project=false", async () => {
    const res = await createOrder({ ...baseOrder, create_project: false });
    if (!("id" in res)) failResult(res);
    createdOrderIds.push(res.id!);
    expect(res.project_id).toBeUndefined();
  });
});

describe("createOrderFromQuotation (auto-project gets its OWN number)", () => {
  it("links a project whose number is from the project sequence, distinct from the order number", async () => {
    // Seed a minimal quotation (no items/sections needed — createOrderFrom
    // Quotation copies whatever is present and both arrays may be empty).
    const quotation = await prisma.quotations.create({
      data: {
        quotation_number: `Q-FROMQ-${Date.now()}`,
        status: "active",
        currency: "CZK",
        language: "cs",
      },
    });
    createdQuotationIds.push(quotation.id);

    const res = await createOrderFromQuotation({
      quotationId: quotation.id,
      customerOrderNumber: "PO-FROMQ",
    });
    if (!("data" in res) || !res.data) failResult(res);
    const orderId = res.data.order_id;
    createdOrderIds.push(orderId);

    const order = await prisma.orders.findUnique({ where: { id: orderId } });
    const project = await prisma.projects.findFirst({
      where: { order_id: orderId },
    });
    if (project) createdProjectIds.push(project.id);

    // Primary flow: the auto-created project must get a non-empty number from
    // the dedicated `project` sequence (code 73), DISTINCT from the order's
    // shared number (code 71). Pre-split they shared one number.
    expect(project).toBeTruthy();
    expect(project?.project_number).toBeTruthy();
    expect(order?.order_number).toBeTruthy();
    expect(project?.project_number).not.toBe(order?.order_number);
    expect(project?.order_id).toBe(orderId);
  });

  it("rejects a DRAFT offer (a numberless 'ordered' offer must never exist)", async () => {
    const draft = await prisma.quotations.create({
      data: { status: "draft", currency: "CZK", language: "cs" },
    });
    createdQuotationIds.push(draft.id);

    const res = await createOrderFromQuotation({ quotationId: draft.id });
    expect("error" in res).toBe(true);
    if ("error" in res) {
      expect(res.status).toBe(400);
      expect(res.error).toContain('ve stavu "draft"');
    }
    // The offer is untouched — still a numberless draft.
    const row = await prisma.quotations.findUnique({ where: { id: draft.id } });
    expect(row!.status).toBe("draft");
    expect(row!.quotation_number).toBeNull();
    expect(row!.order_id).toBeNull();
  });

  it("rejects an INVALIDATED offer (terminal status — no ordered transition)", async () => {
    const invalidated = await prisma.quotations.create({
      data: {
        quotation_number: `Q-INVAL-${Date.now()}`,
        status: "invalidated",
        currency: "CZK",
        language: "cs",
      },
    });
    createdQuotationIds.push(invalidated.id);

    const res = await createOrderFromQuotation({
      quotationId: invalidated.id,
    });
    expect("error" in res).toBe(true);
    if ("error" in res) {
      expect(res.status).toBe(400);
      expect(res.error).toContain('ve stavu "invalidated"');
    }
  });
});

describe("deleteOrder reverts the source offer (stuck-ordered regression)", () => {
  it("returns the linked quotation to active with order_id cleared", async () => {
    const quotation = await prisma.quotations.create({
      data: {
        quotation_number: `Q-REVERT-${Date.now()}`,
        status: "active",
        currency: "CZK",
        language: "cs",
      },
    });
    createdQuotationIds.push(quotation.id);

    const res = await createOrderFromQuotation({ quotationId: quotation.id });
    if (!("data" in res) || !res.data) failResult(res);
    const project = await prisma.projects.findFirst({
      where: { order_id: res.data.order_id },
    });
    if (project) createdProjectIds.push(project.id);

    const afterCreate = await prisma.quotations.findUnique({
      where: { id: quotation.id },
    });
    expect(afterCreate!.status).toBe("ordered");
    expect(afterCreate!.order_id).toBe(res.data.order_id);

    const del = await deleteOrder(res.data.order_id);
    if ("error" in del) failResult(del);

    // The offer must be orderable again — `ordered` with no order_id is a
    // dead end (the transition table only allows ordered -> invalidated).
    const afterDelete = await prisma.quotations.findUnique({
      where: { id: quotation.id },
    });
    expect(afterDelete!.order_id).toBeNull();
    expect(afterDelete!.status).toBe("active");
  });
});

describe("deleteOrder releases the project number (release regression)", () => {
  it("frees the auto-created project's number back to the project sequence", async () => {
    // The project sequence is reset in afterEach, so the preview before the
    // create is the number createOrder will consume for the project.
    const previewBefore = await previewProjectNumber();

    const res = await createOrder({
      ...baseOrder,
      scope_title: "Smazatelná zakázka",
      create_project: true,
    });
    if (!("id" in res)) failResult(res);
    const orderId = res.id!;
    expect(res.project_id).toBeTruthy();
    const project = await prisma.projects.findUnique({
      where: { id: res.project_id! },
    });
    expect(project?.project_number).toBe(previewBefore);

    // After the order delete, the project row is gone AND its number must have
    // been released (decremented) so the preview returns the freed number — not
    // a permanent gap. This is the fix #1 regression guard.
    const del = await deleteOrder(orderId);
    if ("error" in del) failResult(del);

    const gone = await prisma.projects.findUnique({
      where: { id: res.project_id! },
    });
    expect(gone).toBeNull();

    const previewAfter = await previewProjectNumber();
    expect(previewAfter).toBe(previewBefore);
  });
});

describe("separate order vs project sequences (coherence)", () => {
  it("order → standalone project → order produce three distinct numbers (from separate sequences)", async () => {
    const o1 = await createOrder({ ...baseOrder, create_project: true });
    if (!("id" in o1)) failResult(o1);
    createdOrderIds.push(o1.id!);
    if (o1.project_id) createdProjectIds.push(o1.project_id);

    const p = await createProject({ name: "Mezi-projekt", status: "aktivni" });
    if (!("project_number" in p)) failResult(p);
    createdProjectIds.push(p.id);

    const o2 = await createOrder({ ...baseOrder, create_project: false });
    if (!("id" in o2)) failResult(o2);
    createdOrderIds.push(o2.id!);

    // All three numbers must be present (non-null) AND distinct. Orders draw
    // from the `shared` sequence (code 71); projects from the dedicated
    // `project` sequence (code 73), so they never collide.
    expect(o1.order_number).toBeTruthy();
    expect(p.project_number).toBeTruthy();
    expect(o2.order_number).toBeTruthy();
    expect(
      new Set([o1.order_number, p.project_number, o2.order_number]).size,
    ).toBe(3);
  });
});

describe("createProject (manual, standalone)", () => {
  it("assigns a project number and is not linked to an order", async () => {
    const result = await createProject({
      name: "Test projekt",
      status: "aktivni",
    });
    if (!("project_number" in result)) failResult(result);
    createdProjectIds.push(result.id);
    expect(typeof result.project_number).toBe("string");
    expect(result.project_number!.length).toBeGreaterThan(0);
    expect(result.order_id).toBeNull();
  });

  it("gives consecutive standalone projects distinct numbers", async () => {
    const a = await createProject({ name: "Projekt A", status: "aktivni" });
    const b = await createProject({ name: "Projekt B", status: "aktivni" });
    if ("project_number" in a) createdProjectIds.push(a.id);
    if ("project_number" in b) createdProjectIds.push(b.id);
    expect("project_number" in a && "project_number" in b).toBe(true);
    if ("project_number" in a && "project_number" in b) {
      expect(a.project_number).not.toBe(b.project_number);
    }
  });
});

describe("createOrder with price line item + attachment", () => {
  it("stores a single line item with the entered price", async () => {
    const res = await createOrder({
      ...baseOrder,
      create_project: false,
      items: [
        {
          description: "Práce",
          quantity: 1,
          unit_price: 12345,
          is_included_in_total: true,
        },
      ],
    });
    if (!("id" in res)) failResult(res);
    createdOrderIds.push(res.id!);
    const items = await prisma.order_items.findMany({
      where: { order_id: res.id },
    });
    expect(items.length).toBe(1);
    expect(Number(items[0].unit_price)).toBe(12345);
  });

  it("stores an uploaded PO attachment with the exact bytes", async () => {
    const buf = Buffer.from("%PDF-1.4 fake-attachment-bytes-éí");
    const res = await createOrder(
      { ...baseOrder, create_project: false },
      buf,
      "po.pdf",
    );
    if (!("id" in res)) failResult(res);
    createdOrderIds.push(res.id!);
    const order = await prisma.orders.findUnique({
      where: { id: res.id },
      select: { attachment_name: true, attachment_data: true },
    });
    expect(order?.attachment_name).toBe("po.pdf");
    // Verify the stored content/size, not just truthiness: round-trip the
    // bytes and compare length + value against what we uploaded.
    expect(order?.attachment_data).toBeTruthy();
    const stored = Buffer.from(order!.attachment_data!);
    expect(stored.length).toBe(buf.length);
    expect(stored.equals(buf)).toBe(true);
  });
});
