import { describe, expect, it, vi } from "vitest";
import { EstimateStatus } from "@capakraken/shared";
import { createEstimate } from "../use-cases/estimate/create-estimate.js";
import { cloneEstimate } from "../use-cases/estimate/clone-estimate.js";
import { listEstimates } from "../use-cases/estimate/list-estimates.js";
import {
  submitEstimateVersion,
  approveEstimateVersion,
  createEstimateRevision,
} from "../use-cases/estimate/version-actions.js";

// ---------------------------------------------------------------------------
// Shared fixtures
// ---------------------------------------------------------------------------

const BASE_VERSION: {
  id: string;
  estimateId: string;
  versionNumber: number;
  label: string;
  status: string;
  lockedAt: Date | null;
  notes: string | null;
  projectSnapshot: Record<string, unknown>;
  createdAt: Date;
  updatedAt: Date;
  assumptions: never[];
  scopeItems: never[];
  demandLines: never[];
  resourceSnapshots: never[];
  metrics: never[];
  exports: never[];
} = {
  id: "ver_1",
  estimateId: "est_1",
  versionNumber: 1,
  label: "v1",
  status: "WORKING",
  lockedAt: null,
  notes: null,
  projectSnapshot: {},
  createdAt: new Date("2026-01-01"),
  updatedAt: new Date("2026-01-01"),
  assumptions: [],
  scopeItems: [],
  demandLines: [],
  resourceSnapshots: [],
  metrics: [],
  exports: [],
};

const BASE_ESTIMATE = {
  id: "est_1",
  name: "Test Estimate",
  projectId: "proj_1",
  opportunityId: null,
  baseCurrency: "USD",
  status: "DRAFT",
  latestVersionNumber: 1,
  createdAt: new Date("2026-01-01"),
  updatedAt: new Date("2026-01-01"),
  project: {
    id: "proj_1",
    shortCode: "TP",
    name: "Test Project",
    status: "ACTIVE",
    startDate: new Date(),
    endDate: new Date(),
  },
  versions: [BASE_VERSION],
};

// ---------------------------------------------------------------------------
// createEstimate
// ---------------------------------------------------------------------------

describe("createEstimate", () => {
  function makeDb(createdEstimate = BASE_ESTIMATE) {
    return {
      project: {
        findUnique: vi.fn().mockResolvedValue({
          id: "proj_1",
          shortCode: "TP",
          name: "Test Project",
          status: "ACTIVE",
          startDate: new Date("2026-01-01"),
          endDate: new Date("2026-12-31"),
          orderType: "FIXED",
          allocationType: "FULL",
          winProbability: null,
          budgetCents: null,
          responsiblePerson: null,
        }),
      },
      estimate: {
        create: vi.fn().mockResolvedValue(createdEstimate),
      },
    };
  }

  const minimalInput = {
    projectId: "proj_1",
    name: "My Estimate",
    baseCurrency: "USD",
    status: EstimateStatus.DRAFT,
    assumptions: [],
    scopeItems: [],
    demandLines: [],
    resourceSnapshots: [],
    metrics: [],
  };

  it("creates an estimate and returns the full detail object", async () => {
    const db = makeDb();
    const result = await createEstimate(db as never, minimalInput);

    expect(db.estimate.create).toHaveBeenCalledOnce();
    expect(result.id).toBe("est_1");
    expect(result.name).toBe("Test Estimate");
  });

  it("passes projectId to the estimate.create call", async () => {
    const db = makeDb();
    await createEstimate(db as never, minimalInput);

    const createData = db.estimate.create.mock.calls[0]![0].data;
    expect(createData.projectId).toBe("proj_1");
  });

  it("returns an estimate when no projectId is provided", async () => {
    const db = makeDb({ ...BASE_ESTIMATE, projectId: null as never, project: null as never });
    db.project.findUnique.mockResolvedValue(null);
    const inputWithoutProject = { ...minimalInput, projectId: undefined };
    const result = await createEstimate(db as never, inputWithoutProject);

    expect(result).toBeDefined();
    expect(db.estimate.create).toHaveBeenCalledOnce();
  });
});

// ---------------------------------------------------------------------------
// cloneEstimate
// ---------------------------------------------------------------------------

describe("cloneEstimate", () => {
  const SOURCE_VERSION = {
    ...BASE_VERSION,
    id: "ver_src",
    status: "WORKING",
    lockedAt: new Date("2026-01-15"),
  };

  const SOURCE_ESTIMATE = {
    ...BASE_ESTIMATE,
    id: "est_src",
    name: "Original",
    versions: [SOURCE_VERSION],
  };

  const CLONED_ESTIMATE = {
    ...BASE_ESTIMATE,
    id: "est_clone",
    name: "Copy of Original",
    status: "DRAFT",
  };

  function makeDb(source = SOURCE_ESTIMATE as typeof SOURCE_ESTIMATE | null) {
    return {
      project: {
        findUnique: vi.fn().mockResolvedValue({
          id: "proj_1",
          shortCode: "TP",
          name: "Test Project",
          status: "ACTIVE",
          startDate: new Date("2026-01-01"),
          endDate: new Date("2026-12-31"),
          orderType: "FIXED",
          allocationType: "FULL",
          winProbability: null,
          budgetCents: null,
          responsiblePerson: null,
        }),
      },
      estimate: {
        findUnique: vi.fn().mockResolvedValue(source),
        create: vi.fn().mockResolvedValue(CLONED_ESTIMATE),
      },
    };
  }

  it("clones an estimate with a default name", async () => {
    const db = makeDb();
    const result = await cloneEstimate(db as never, { sourceEstimateId: "est_src" });

    expect(db.estimate.create).toHaveBeenCalledOnce();
    const createData = db.estimate.create.mock.calls[0]![0].data;
    expect(createData.name).toBe("Copy of Original");
    expect(result.id).toBe("est_clone");
  });

  it("uses the provided name override", async () => {
    const db = makeDb();
    await cloneEstimate(db as never, { sourceEstimateId: "est_src", name: "Custom Clone" });

    const createData = db.estimate.create.mock.calls[0]![0].data;
    expect(createData.name).toBe("Custom Clone");
  });

  it("throws when the source estimate does not exist", async () => {
    const db = makeDb(null);
    await expect(cloneEstimate(db as never, { sourceEstimateId: "nonexistent" })).rejects.toThrow(
      "Source estimate not found",
    );
  });

  it("throws when the source estimate has no versions", async () => {
    const db = makeDb({ ...SOURCE_ESTIMATE, versions: [] });
    await expect(cloneEstimate(db as never, { sourceEstimateId: "est_src" })).rejects.toThrow(
      "Source estimate has no versions",
    );
  });
});

// ---------------------------------------------------------------------------
// listEstimates
// ---------------------------------------------------------------------------

describe("listEstimates", () => {
  const LIST_ITEMS = [
    {
      ...BASE_ESTIMATE,
      project: { id: "proj_1", shortCode: "TP", name: "Test Project", status: "ACTIVE" },
      versions: [
        { id: "ver_1", versionNumber: 1, label: "v1", status: "WORKING", updatedAt: new Date() },
      ],
    },
  ];

  function makeDb(items = LIST_ITEMS) {
    return {
      estimate: {
        findMany: vi.fn().mockResolvedValue(items),
      },
    };
  }

  it("returns all estimates when no filters are given", async () => {
    const db = makeDb();
    const result = await listEstimates(db as never);

    expect(db.estimate.findMany).toHaveBeenCalledOnce();
    expect(result).toHaveLength(1);
  });

  it("returns an empty array when there are no estimates", async () => {
    const db = makeDb([]);
    const result = await listEstimates(db as never, {});

    expect(result).toHaveLength(0);
  });

  it("passes projectId filter to findMany", async () => {
    const db = makeDb();
    await listEstimates(db as never, { projectId: "proj_1" });

    const where = db.estimate.findMany.mock.calls[0]![0].where;
    expect(where.projectId).toBe("proj_1");
  });

  it("passes status filter to findMany", async () => {
    const db = makeDb([]);
    await listEstimates(db as never, { status: "APPROVED" as never });

    const where = db.estimate.findMany.mock.calls[0]![0].where;
    expect(where.status).toBe("APPROVED");
  });

  it("builds an OR query when a search query is provided", async () => {
    const db = makeDb();
    await listEstimates(db as never, { query: "alpha" });

    const where = db.estimate.findMany.mock.calls[0]![0].where;
    expect(where.OR).toBeDefined();
    expect(where.OR).toHaveLength(2);
  });
});

// ---------------------------------------------------------------------------
// submitEstimateVersion
// ---------------------------------------------------------------------------

describe("submitEstimateVersion", () => {
  function makeTransactionDb(
    estimateBeforeUpdate = BASE_ESTIMATE,
    estimateAfterUpdate = BASE_ESTIMATE,
  ) {
    const txMock = {
      estimateVersion: {
        updateMany: vi.fn().mockResolvedValue({}),
        update: vi.fn().mockResolvedValue({}),
      },
      estimate: {
        update: vi.fn().mockResolvedValue({}),
      },
    };

    return {
      estimate: {
        findUnique: vi
          .fn()
          .mockResolvedValueOnce(estimateBeforeUpdate)
          .mockResolvedValueOnce(estimateAfterUpdate),
      },
      $transaction: vi
        .fn()
        .mockImplementation((cb: (tx: typeof txMock) => Promise<void>) => cb(txMock)),
    };
  }

  const WORKING_ESTIMATE = {
    ...BASE_ESTIMATE,
    versions: [{ ...BASE_VERSION, status: "WORKING" }],
  };

  const SUBMITTED_ESTIMATE = {
    ...BASE_ESTIMATE,
    status: "IN_REVIEW",
    versions: [{ ...BASE_VERSION, status: "SUBMITTED" }],
  };

  it("transitions a WORKING version to SUBMITTED and returns updated estimate", async () => {
    const db = makeTransactionDb(WORKING_ESTIMATE, SUBMITTED_ESTIMATE);
    const result = await submitEstimateVersion(db as never, { estimateId: "est_1" });

    expect(db.$transaction).toHaveBeenCalledOnce();
    expect(result.status).toBe("IN_REVIEW");
  });

  it("throws when the estimate does not exist", async () => {
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(null) },
      $transaction: vi.fn(),
    };
    await expect(submitEstimateVersion(db as never, { estimateId: "nonexistent" })).rejects.toThrow(
      "Estimate not found",
    );
  });

  it("throws when there is no WORKING version", async () => {
    const estimateNoWorking = {
      ...BASE_ESTIMATE,
      versions: [{ ...BASE_VERSION, status: "SUBMITTED" }],
    };
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimateNoWorking) },
      $transaction: vi.fn(),
    };
    await expect(submitEstimateVersion(db as never, { estimateId: "est_1" })).rejects.toThrow(
      "Estimate has no working version",
    );
  });
});

// ---------------------------------------------------------------------------
// approveEstimateVersion
// ---------------------------------------------------------------------------

describe("approveEstimateVersion", () => {
  function makeTransactionDb(
    estimateBeforeUpdate = BASE_ESTIMATE,
    estimateAfterUpdate = BASE_ESTIMATE,
  ) {
    const txMock = {
      estimateVersion: {
        updateMany: vi.fn().mockResolvedValue({}),
        update: vi.fn().mockResolvedValue({}),
      },
      estimate: {
        update: vi.fn().mockResolvedValue({}),
      },
    };

    return {
      estimate: {
        findUnique: vi
          .fn()
          .mockResolvedValueOnce(estimateBeforeUpdate)
          .mockResolvedValueOnce(estimateAfterUpdate),
      },
      $transaction: vi
        .fn()
        .mockImplementation((cb: (tx: typeof txMock) => Promise<void>) => cb(txMock)),
    };
  }

  const SUBMITTED_ESTIMATE = {
    ...BASE_ESTIMATE,
    status: "IN_REVIEW",
    versions: [{ ...BASE_VERSION, status: "SUBMITTED", lockedAt: new Date("2026-01-10") }],
  };

  const APPROVED_ESTIMATE = {
    ...BASE_ESTIMATE,
    status: "APPROVED",
    versions: [{ ...BASE_VERSION, status: "APPROVED", lockedAt: new Date("2026-01-10") }],
  };

  it("transitions a SUBMITTED version to APPROVED and returns updated estimate", async () => {
    const db = makeTransactionDb(SUBMITTED_ESTIMATE, APPROVED_ESTIMATE);
    const result = await approveEstimateVersion(db as never, { estimateId: "est_1" });

    expect(db.$transaction).toHaveBeenCalledOnce();
    expect(result.status).toBe("APPROVED");
  });

  it("throws when the estimate does not exist", async () => {
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(null) },
      $transaction: vi.fn(),
    };
    await expect(
      approveEstimateVersion(db as never, { estimateId: "nonexistent" }),
    ).rejects.toThrow("Estimate not found");
  });

  it("throws when there is no SUBMITTED version", async () => {
    const estimateNoSubmitted = {
      ...BASE_ESTIMATE,
      versions: [{ ...BASE_VERSION, status: "WORKING" }],
    };
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimateNoSubmitted) },
      $transaction: vi.fn(),
    };
    await expect(approveEstimateVersion(db as never, { estimateId: "est_1" })).rejects.toThrow(
      "Estimate has no submitted version",
    );
  });
});

// ---------------------------------------------------------------------------
// createEstimateRevision
// ---------------------------------------------------------------------------

describe("createEstimateRevision", () => {
  function makeTransactionDb(
    estimateBeforeUpdate = BASE_ESTIMATE,
    estimateAfterUpdate = BASE_ESTIMATE,
  ) {
    const txMock = {
      estimateVersion: {
        create: vi.fn().mockResolvedValue({
          ...BASE_VERSION,
          id: "ver_new",
          versionNumber: 2,
          status: "WORKING",
        }),
      },
      estimateAssumption: { createMany: vi.fn().mockResolvedValue({}) },
      scopeItem: { create: vi.fn().mockResolvedValue({ id: "scope_new" }) },
      estimateDemandLine: { createMany: vi.fn().mockResolvedValue({}) },
      resourceCostSnapshot: { createMany: vi.fn().mockResolvedValue({}) },
      estimateMetric: { createMany: vi.fn().mockResolvedValue({}) },
      estimate: { update: vi.fn().mockResolvedValue({}) },
    };

    return {
      estimate: {
        findUnique: vi
          .fn()
          .mockResolvedValueOnce(estimateBeforeUpdate)
          .mockResolvedValueOnce(estimateAfterUpdate),
      },
      $transaction: vi
        .fn()
        .mockImplementation((cb: (tx: typeof txMock) => Promise<void>) => cb(txMock)),
    };
  }

  const LOCKED_VERSION = {
    ...BASE_VERSION,
    id: "ver_locked",
    status: "SUBMITTED",
    lockedAt: new Date("2026-01-10"),
    assumptions: [],
    scopeItems: [],
    demandLines: [],
    resourceSnapshots: [],
    metrics: [],
    projectSnapshot: {},
  };

  const APPROVED_ESTIMATE = {
    ...BASE_ESTIMATE,
    status: "APPROVED",
    latestVersionNumber: 1,
    versions: [LOCKED_VERSION],
  };

  const REVISED_ESTIMATE = {
    ...BASE_ESTIMATE,
    status: "DRAFT",
    latestVersionNumber: 2,
    versions: [
      { ...BASE_VERSION, id: "ver_new", versionNumber: 2, status: "WORKING" },
      LOCKED_VERSION,
    ],
  };

  it("creates a new WORKING revision from a locked version", async () => {
    const db = makeTransactionDb(APPROVED_ESTIMATE, REVISED_ESTIMATE);
    const result = await createEstimateRevision(db as never, { estimateId: "est_1" });

    expect(db.$transaction).toHaveBeenCalledOnce();
    expect(result.latestVersionNumber).toBe(2);
    expect(result.status).toBe("DRAFT");
  });

  it("throws when the estimate does not exist", async () => {
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(null) },
      $transaction: vi.fn(),
    };
    await expect(
      createEstimateRevision(db as never, { estimateId: "nonexistent" }),
    ).rejects.toThrow("Estimate not found");
  });

  it("throws when a WORKING version already exists", async () => {
    const db = {
      estimate: {
        findUnique: vi.fn().mockResolvedValue({
          ...BASE_ESTIMATE,
          versions: [{ ...BASE_VERSION, status: "WORKING" }],
        }),
      },
      $transaction: vi.fn(),
    };
    await expect(createEstimateRevision(db as never, { estimateId: "est_1" })).rejects.toThrow(
      "Estimate already has a working version",
    );
  });

  it("throws when there is no locked version to revise", async () => {
    const db = {
      estimate: {
        findUnique: vi.fn().mockResolvedValue({
          ...BASE_ESTIMATE,
          versions: [],
        }),
      },
      $transaction: vi.fn(),
    };
    await expect(createEstimateRevision(db as never, { estimateId: "est_1" })).rejects.toThrow(
      "Estimate has no locked version to revise",
    );
  });
});
