import { SystemRole, PermissionKey } from "@capakraken/shared";
import { describe, expect, it, vi, beforeEach } from "vitest";

// Mock application layer
const listEstimatesMock = vi.fn();
const getEstimateByIdMock = vi.fn();
const submitEstimateVersionMock = vi.fn();
const approveEstimateVersionMock = vi.fn();
const createEstimateRevisionMock = vi.fn();

vi.mock("@capakraken/application", () => ({
  listEstimates: (...args: unknown[]) => listEstimatesMock(...args),
  getEstimateById: (...args: unknown[]) => getEstimateByIdMock(...args),
  submitEstimateVersion: (...args: unknown[]) => submitEstimateVersionMock(...args),
  approveEstimateVersion: (...args: unknown[]) => approveEstimateVersionMock(...args),
  createEstimateRevision: (...args: unknown[]) => createEstimateRevisionMock(...args),
}));

// Mock engine
vi.mock("@capakraken/engine", () => ({
  summarizeEstimateDemandLines: vi.fn().mockReturnValue({
    totalHours: 100,
    totalCostCents: 500000,
    totalPriceCents: 750000,
    marginCents: 250000,
    marginPercent: 33.33,
  }),
}));

import { createCallerFactory, createTRPCRouter } from "../trpc.js";
import { estimateReadProcedures } from "../router/estimate-read.js";
import { estimateVersionWorkflowProcedures } from "../router/estimate-version-workflow.js";
import { rethrowEstimateRouterError } from "../router/estimate-procedure-support.js";

const readRouter = createTRPCRouter(estimateReadProcedures);
const workflowRouter = createTRPCRouter(estimateVersionWorkflowProcedures);
const createReadCaller = createCallerFactory(readRouter);
const createWorkflowCaller = createCallerFactory(workflowRouter);

function createControllerCaller(db: Record<string, unknown>) {
  return createReadCaller({
    session: {
      user: { email: "ctrl@example.com", name: "Controller", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: { id: "user_ctrl", systemRole: SystemRole.CONTROLLER, permissionOverrides: null },
  });
}

function createManagerCaller(db: Record<string, unknown>) {
  return createWorkflowCaller({
    session: {
      user: { email: "mgr@example.com", name: "Manager", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: { id: "user_mgr", systemRole: SystemRole.MANAGER, permissionOverrides: null },
  });
}

// ─── Shared fixture ───────────────────────────────────────────────────────────

const versionFixture = {
  id: "v1",
  versionNumber: 1,
  label: "v1",
  status: "WORKING",
  notes: null,
  lockedAt: null,
  createdAt: new Date(),
  updatedAt: new Date(),
  assumptions: [{ id: "a1", category: "TIMELINE", key: "k1", label: "Assumption 1" }],
  scopeItems: [{ id: "s1", scopeType: "FEATURE", sequenceNo: 1, name: "Scope 1" }],
  demandLines: [
    {
      id: "d1",
      name: "Dev",
      chapter: "Engineering",
      hours: 80,
      costTotalCents: 400000,
      priceTotalCents: 600000,
      currency: "EUR",
    },
    {
      id: "d2",
      name: "QA",
      chapter: "Engineering",
      hours: 20,
      costTotalCents: 100000,
      priceTotalCents: 150000,
      currency: "EUR",
    },
  ],
  resourceSnapshots: [
    {
      id: "rs1",
      displayName: "Dev 1",
      chapter: "Engineering",
      currency: "EUR",
      lcrCents: 5000,
      ucrCents: 7500,
    },
  ],
  exports: [],
};

const estimateFixture = {
  id: "est_1",
  name: "Test Estimate",
  status: "DRAFT",
  baseCurrency: "EUR",
  versions: [versionFixture],
};

// ─── rethrowEstimateRouterError ───────────────────────────────────────────────

describe("rethrowEstimateRouterError", () => {
  it("throws a TRPCError with the matching code when the error message is in the rule list", () => {
    const rules = [{ code: "NOT_FOUND" as const, messages: ["Estimate not found"] }];

    expect(() => rethrowEstimateRouterError(new Error("Estimate not found"), rules)).toThrow(
      expect.objectContaining({ code: "NOT_FOUND", message: "Estimate not found" }),
    );
  });

  it("re-throws the original Error when no rule matches", () => {
    const original = new Error("Something unexpected");
    const rules = [{ code: "NOT_FOUND" as const, messages: ["Estimate not found"] }];

    expect(() => rethrowEstimateRouterError(original, rules)).toThrow(original);
  });

  it("re-throws non-Error values as-is", () => {
    const thrown = { someObject: true };

    try {
      rethrowEstimateRouterError(thrown, []);
      // should never reach here
      expect.fail("Expected rethrowEstimateRouterError to throw");
    } catch (caught) {
      expect(caught).toBe(thrown);
    }
  });

  it("matches via predicate function and throws the mapped TRPCError", () => {
    const rules = [
      {
        code: "PRECONDITION_FAILED" as const,
        predicates: [(msg: string) => msg.startsWith("Project window has no working days")],
      },
    ];

    expect(() =>
      rethrowEstimateRouterError(
        new Error("Project window has no working days for demand line dev"),
        rules,
      ),
    ).toThrow(expect.objectContaining({ code: "PRECONDITION_FAILED" }));
  });
});

// ─── estimate read: list ──────────────────────────────────────────────────────

describe("estimate read: list", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("delegates to listEstimates with the correct arguments", async () => {
    listEstimatesMock.mockResolvedValue([]);
    const caller = createControllerCaller({});

    await caller.list({});

    expect(listEstimatesMock).toHaveBeenCalledOnce();
  });

  it("returns the result from listEstimates", async () => {
    const estimates = [
      { id: "est_1", name: "Alpha", status: "DRAFT" },
      { id: "est_2", name: "Beta", status: "IN_REVIEW" },
    ];
    listEstimatesMock.mockResolvedValue(estimates);
    const caller = createControllerCaller({});

    const result = await caller.list({});

    expect(result).toEqual(estimates);
  });
});

// ─── estimate read: getById ───────────────────────────────────────────────────

describe("estimate read: getById", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("returns the estimate when getEstimateById resolves a value", async () => {
    const estimate = { id: "est_1", name: "My Estimate", status: "DRAFT" };
    getEstimateByIdMock.mockResolvedValue(estimate);
    const caller = createControllerCaller({});

    const result = await caller.getById({ id: "est_1" });

    expect(result).toEqual(estimate);
  });

  it("throws NOT_FOUND when getEstimateById returns null", async () => {
    getEstimateByIdMock.mockResolvedValue(null);
    const caller = createControllerCaller({});

    await expect(caller.getById({ id: "est_missing" })).rejects.toMatchObject({
      code: "NOT_FOUND",
      message: "Estimate not found",
    });
  });
});

// ─── estimate read: getVersionSnapshot ───────────────────────────────────────

describe("estimate read: getVersionSnapshot", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("returns aggregated snapshot with totals, chapter breakdown, and scope breakdown", async () => {
    const db = {
      estimate: {
        findUnique: vi.fn().mockResolvedValue(estimateFixture),
      },
    };
    const caller = createControllerCaller(db);

    const result = await caller.getVersionSnapshot({ estimateId: "est_1" });

    expect(result.estimate).toEqual({
      id: "est_1",
      name: "Test Estimate",
      status: "DRAFT",
      baseCurrency: "EUR",
    });
    expect(result.version.id).toBe("v1");
    expect(result.totals).toEqual({
      hours: 100,
      costTotalCents: 500000,
      priceTotalCents: 750000,
      marginCents: 250000,
      marginPercent: 33.33,
    });
    // Both demand lines belong to "Engineering" — one chapter entry
    expect(result.chapterBreakdown).toHaveLength(1);
    expect(result.chapterBreakdown[0]).toMatchObject({
      chapter: "Engineering",
      lineCount: 2,
      hours: 100,
      costTotalCents: 500000,
      priceTotalCents: 750000,
    });
    // One scope item of type FEATURE
    expect(result.scopeTypeBreakdown).toEqual([{ scopeType: "FEATURE", count: 1 }]);
    // One assumption of category TIMELINE
    expect(result.assumptionCategoryBreakdown).toEqual([{ category: "TIMELINE", count: 1 }]);
    expect(result.counts).toEqual({
      assumptions: 1,
      scopeItems: 1,
      demandLines: 2,
      resourceSnapshots: 1,
      exports: 0,
    });
  });

  it("throws NOT_FOUND when the estimate does not exist", async () => {
    const db = {
      estimate: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
    };
    const caller = createControllerCaller(db);

    await expect(caller.getVersionSnapshot({ estimateId: "est_missing" })).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("throws NOT_FOUND when the estimate exists but has no matching versions", async () => {
    const db = {
      estimate: {
        findUnique: vi.fn().mockResolvedValue({ ...estimateFixture, versions: [] }),
      },
    };
    const caller = createControllerCaller(db);

    await expect(caller.getVersionSnapshot({ estimateId: "est_1" })).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });
});

// ─── estimate workflow: submitVersion ────────────────────────────────────────

describe("estimate workflow: submitVersion", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  function buildManagerDb() {
    const auditLogCreate = vi.fn().mockResolvedValue({});
    const txMock = { auditLog: { create: auditLogCreate } };
    const db = {
      $transaction: vi
        .fn()
        .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      _txMock: txMock,
    };
    return db;
  }

  it("calls submitEstimateVersion inside the transaction and creates an audit log entry", async () => {
    const submittedEstimate = {
      id: "est_1",
      status: "IN_REVIEW",
      versions: [{ id: "v1", status: "SUBMITTED" }],
    };
    submitEstimateVersionMock.mockResolvedValue(submittedEstimate);

    const db = buildManagerDb();
    const caller = createManagerCaller(db);

    const result = await caller.submitVersion({ estimateId: "est_1" });

    expect(submitEstimateVersionMock).toHaveBeenCalledOnce();
    expect(db._txMock.auditLog.create).toHaveBeenCalledOnce();
    expect(result).toEqual(submittedEstimate);
  });

  it("includes the submitted version id in the audit log changes", async () => {
    const submittedEstimate = {
      id: "est_1",
      status: "IN_REVIEW",
      versions: [{ id: "v1", status: "SUBMITTED" }],
    };
    submitEstimateVersionMock.mockResolvedValue(submittedEstimate);

    const db = buildManagerDb();
    const caller = createManagerCaller(db);

    await caller.submitVersion({ estimateId: "est_1" });

    const auditCall = db._txMock.auditLog.create.mock.calls[0][0] as {
      data: { changes: { after: Record<string, unknown> } };
    };
    expect(auditCall.data.changes.after).toMatchObject({
      id: "est_1",
      status: "IN_REVIEW",
      submittedVersionId: "v1",
    });
  });

  it("throws PRECONDITION_FAILED when submitEstimateVersion rejects with a known message", async () => {
    submitEstimateVersionMock.mockRejectedValue(
      new Error("Only working versions can be submitted"),
    );

    const db = buildManagerDb();
    const caller = createManagerCaller(db);

    await expect(
      caller.submitVersion({ estimateId: "est_1", versionId: "v_bad" }),
    ).rejects.toMatchObject({ code: "PRECONDITION_FAILED" });
  });
});

// ─── estimate workflow: approveVersion ───────────────────────────────────────

describe("estimate workflow: approveVersion", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  function buildManagerDb() {
    const auditLogCreate = vi.fn().mockResolvedValue({});
    const txMock = { auditLog: { create: auditLogCreate } };
    const db = {
      $transaction: vi
        .fn()
        .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      _txMock: txMock,
    };
    return db;
  }

  it("calls approveEstimateVersion inside the transaction and creates an audit log entry", async () => {
    const approvedEstimate = {
      id: "est_1",
      status: "APPROVED",
      versions: [{ id: "v1", status: "APPROVED" }],
    };
    approveEstimateVersionMock.mockResolvedValue(approvedEstimate);

    const db = buildManagerDb();
    const caller = createManagerCaller(db);

    const result = await caller.approveVersion({ estimateId: "est_1" });

    expect(approveEstimateVersionMock).toHaveBeenCalledOnce();
    expect(db._txMock.auditLog.create).toHaveBeenCalledOnce();
    expect(result).toEqual(approvedEstimate);
  });

  it("records the approved version id in the audit log", async () => {
    const approvedEstimate = {
      id: "est_1",
      status: "APPROVED",
      versions: [{ id: "v1", status: "APPROVED" }],
    };
    approveEstimateVersionMock.mockResolvedValue(approvedEstimate);

    const db = buildManagerDb();
    const caller = createManagerCaller(db);

    await caller.approveVersion({ estimateId: "est_1" });

    const auditCall = db._txMock.auditLog.create.mock.calls[0][0] as {
      data: { changes: { after: Record<string, unknown> } };
    };
    expect(auditCall.data.changes.after).toMatchObject({
      id: "est_1",
      status: "APPROVED",
      approvedVersionId: "v1",
    });
  });
});

// ─── estimate workflow: createRevision ───────────────────────────────────────

describe("estimate workflow: createRevision", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  function buildManagerDb() {
    const auditLogCreate = vi.fn().mockResolvedValue({});
    const txMock = { auditLog: { create: auditLogCreate } };
    const db = {
      $transaction: vi
        .fn()
        .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      _txMock: txMock,
    };
    return db;
  }

  it("calls createEstimateRevision inside the transaction and creates an audit log entry", async () => {
    const revision = {
      id: "est_1",
      status: "DRAFT",
      latestVersionNumber: 2,
      versions: [{ id: "v2", status: "WORKING" }],
    };
    createEstimateRevisionMock.mockResolvedValue(revision);

    const db = buildManagerDb();
    const caller = createManagerCaller(db);

    const result = await caller.createRevision({ estimateId: "est_1" });

    expect(createEstimateRevisionMock).toHaveBeenCalledOnce();
    expect(db._txMock.auditLog.create).toHaveBeenCalledOnce();
    expect(result).toEqual(revision);
  });

  it("records workingVersionId and latestVersionNumber in the audit log", async () => {
    const revision = {
      id: "est_1",
      status: "DRAFT",
      latestVersionNumber: 2,
      versions: [{ id: "v2", status: "WORKING" }],
    };
    createEstimateRevisionMock.mockResolvedValue(revision);

    const db = buildManagerDb();
    const caller = createManagerCaller(db);

    await caller.createRevision({ estimateId: "est_1" });

    const auditCall = db._txMock.auditLog.create.mock.calls[0][0] as {
      data: { changes: { after: Record<string, unknown> } };
    };
    expect(auditCall.data.changes.after).toMatchObject({
      id: "est_1",
      status: "DRAFT",
      latestVersionNumber: 2,
      workingVersionId: "v2",
    });
  });
});
