import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
  generateSharedNumber,
  generateOfferNumber,
  previewSharedNumber,
  isSharedNumberTaken,
  previewOfferNumber,
  isOfferNumberTaken,
  generateProjectNumber,
  previewProjectNumber,
  isProjectNumberTaken,
} from "../services/numbering.service";
import prisma from "../config/database";

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

/**
 * Assert two consecutive document numbers (a) keep an identical format / year
 * prefix and (b) increment by exactly one — without hard-coding which
 * `company_settings` pattern is configured.
 *
 * Some patterns are entirely digits ({YY}{CODE}{NNNN}), so a regex split of
 * "trailing digit run" is ambiguous. Instead we find the longest common prefix
 * of the two strings (the unchanged format/year portion) and parse the
 * remaining differing suffixes as integers — these are the rendered sequences.
 */
function assertStrictIncrement(num1: string, num2: string) {
  expect(num1).not.toBe(num2);
  expect(num2.length).toBe(num1.length); // same width ⇒ same format

  let i = 0;
  while (i < num1.length && num1[i] === num2[i]) i++;
  const commonPrefix = num1.slice(0, i);
  // The format/year portion is shared and non-empty (the numbers don't differ
  // from the very first character).
  expect(commonPrefix.length).toBeGreaterThan(0);

  const seq1 = Number(num1.slice(i));
  const seq2 = Number(num2.slice(i));
  expect(Number.isNaN(seq1)).toBe(false);
  expect(Number.isNaN(seq2)).toBe(false);
  expect(seq2).toBe(seq1 + 1); // strict +1, not merely !==
}

/**
 * Seed the (type, year) sequence row to a high `last_number` so the next
 * generated numbers land far above any real order/quotation in the test DB —
 * the generator's skip-taken retry then never fires and the strict +1
 * assertion is deterministic.
 */
async function seedSequence(type: string, lastNumber: number) {
  await prisma.number_sequences.deleteMany({ where: { type } });
  await prisma.number_sequences.create({
    data: { type, year: YEAR, last_number: lastNumber },
  });
}

describe("generateSharedNumber", () => {
  beforeEach(async () => {
    await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
  });

  afterEach(async () => {
    await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
  });

  it("returns a non-empty string", async () => {
    const num = await generateSharedNumber();
    expect(typeof num).toBe("string");
    expect(num.length).toBeGreaterThan(0);
  });

  it("increments the sequence by exactly 1 and preserves the year/format", async () => {
    // Seed high so neither generated number collides with a real order/project
    // → the skip-taken retry never fires and the increment is strictly +1.
    await seedSequence("shared", 900000);
    const num1 = await generateSharedNumber();
    const num2 = await generateSharedNumber();
    assertStrictIncrement(num1, num2);
  });
});

describe("generateProjectNumber", () => {
  beforeEach(async () => {
    await prisma.number_sequences.deleteMany({ where: { type: "project" } });
  });

  afterEach(async () => {
    await prisma.number_sequences.deleteMany({ where: { type: "project" } });
  });

  it("returns a non-empty string", async () => {
    const num = await generateProjectNumber();
    expect(typeof num).toBe("string");
    expect(num.length).toBeGreaterThan(0);
  });

  it("increments the sequence by exactly 1 and preserves the year/format", async () => {
    // Seed high so neither generated number collides with a real project →
    // the skip-taken retry never fires and the increment is strictly +1.
    await seedSequence("project", 900000);
    const num1 = await generateProjectNumber();
    const num2 = await generateProjectNumber();
    assertStrictIncrement(num1, num2);
  });

  it("previewProjectNumber returns a number not yet taken by a project", async () => {
    const preview = await previewProjectNumber();
    expect(typeof preview).toBe("string");
    expect(preview.length).toBeGreaterThan(0);
    expect(await isProjectNumberTaken(preview)).toBe(false);
  });
});

describe("cross-sequence isolation (shared vs project)", () => {
  beforeEach(async () => {
    await prisma.number_sequences.deleteMany({
      where: { type: { in: ["shared", "project"] } },
    });
  });

  afterEach(async () => {
    await prisma.number_sequences.deleteMany({
      where: { type: { in: ["shared", "project"] } },
    });
  });

  it("generateSharedNumber advances ONLY the shared row; generateProjectNumber ONLY the project row", async () => {
    // Seed the two sequences to DISTINCT known highs so neither generated
    // number collides with a real row (skip-taken retry never fires) and a
    // cross-contamination bug is unambiguous.
    await seedSequence("shared", 900000);
    await seedSequence("project", 500000);

    await generateSharedNumber();
    // After a shared allocation only the shared row may have moved.
    let shared = await prisma.number_sequences.findFirst({
      where: { type: "shared", year: YEAR },
      select: { last_number: true },
    });
    let project = await prisma.number_sequences.findFirst({
      where: { type: "project", year: YEAR },
      select: { last_number: true },
    });
    expect(shared?.last_number).toBe(900001);
    expect(project?.last_number).toBe(500000);

    await generateProjectNumber();
    // After a project allocation only the project row may have moved; the
    // shared row must still sit at 900001.
    shared = await prisma.number_sequences.findFirst({
      where: { type: "shared", year: YEAR },
      select: { last_number: true },
    });
    project = await prisma.number_sequences.findFirst({
      where: { type: "project", year: YEAR },
      select: { last_number: true },
    });
    expect(shared?.last_number).toBe(900001);
    expect(project?.last_number).toBe(500001);
  });
});

describe("generateOfferNumber", () => {
  beforeEach(async () => {
    await prisma.number_sequences.deleteMany({ where: { type: "offer" } });
  });

  afterEach(async () => {
    await prisma.number_sequences.deleteMany({ where: { type: "offer" } });
  });

  it("returns a non-empty string", async () => {
    const num = await generateOfferNumber();
    expect(typeof num).toBe("string");
    expect(num.length).toBeGreaterThan(0);
  });

  it("increments the sequence by exactly 1 and preserves the year/format", async () => {
    await seedSequence("offer", 900000);
    const num1 = await generateOfferNumber();
    const num2 = await generateOfferNumber();
    assertStrictIncrement(num1, num2);
  });
});

describe("previewSharedNumber", () => {
  let createdProjectId: number | null = null;

  beforeEach(async () => {
    await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
  });

  afterEach(async () => {
    if (createdProjectId !== null) {
      await prisma.projects
        .delete({ where: { id: createdProjectId } })
        .catch(() => {});
      createdProjectId = null;
    }
    await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
  });

  it("returns a number not already used by an order or project", async () => {
    const preview = await previewSharedNumber();
    expect(typeof preview).toBe("string");
    expect(await isSharedNumberTaken(preview)).toBe(false);
  });

  it("skips a number already taken when the sequence counter lags behind", async () => {
    // Seed the counter HIGH so the preview lands far above any real
    // order/project in the test DB. Without this, a real row could already
    // hold the first sequence number — making the create collide on the unique
    // column, or making `second !== first` pass for the wrong reason (because
    // the counter happened to already point past `first`).
    await seedSequence("shared", 900000);
    const first = await previewSharedNumber();
    // Preconditions: the seeded preview really is free (so the create below is
    // safe) and is exactly what the generator would assign next.
    expect(await isSharedNumberTaken(first)).toBe(false);

    const project = await prisma.projects.create({
      data: { project_number: first, name: "test-preview-skip-taken" },
    });
    createdProjectId = project.id;

    // The counter still lags behind (no row was consumed), but `first` is now
    // taken — the preview must advance past it instead of re-offering it.
    const second = await previewSharedNumber();
    expect(second).not.toBe(first);
    expect(await isSharedNumberTaken(second)).toBe(false);
  });
});

describe("previewOfferNumber", () => {
  let createdQuotationId: number | null = null;

  beforeEach(async () => {
    await prisma.number_sequences.deleteMany({ where: { type: "offer" } });
  });

  afterEach(async () => {
    if (createdQuotationId !== null) {
      await prisma.quotations
        .delete({ where: { id: createdQuotationId } })
        .catch(() => {});
      createdQuotationId = null;
    }
    await prisma.number_sequences.deleteMany({ where: { type: "offer" } });
  });

  it("returns a number not already used by a quotation", async () => {
    const preview = await previewOfferNumber();
    expect(typeof preview).toBe("string");
    expect(await isOfferNumberTaken(preview)).toBe(false);
  });

  it("skips a number already taken when the sequence counter lags behind", async () => {
    // Seed HIGH so the preview lands above any real quotation in the test DB
    // (see the shared-number test for the rationale): the create can't collide
    // and the skip-advance is genuinely exercised rather than passing because a
    // real row already advanced the preview.
    await seedSequence("offer", 900000);
    const first = await previewOfferNumber();
    expect(await isOfferNumberTaken(first)).toBe(false);

    const quotation = await prisma.quotations.create({
      data: { quotation_number: first },
    });
    createdQuotationId = quotation.id;

    // The counter still lags, but `first` is now taken — the preview must
    // advance past it instead of re-offering it.
    const second = await previewOfferNumber();
    expect(second).not.toBe(first);
    expect(await isOfferNumberTaken(second)).toBe(false);
  });
});
