import { PermissionKey, SystemRole } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { projectRouter } from "../router/project.js";
import { createCallerFactory } from "../trpc.js";

vi.mock("../lib/cache.js", () => ({
  invalidateDashboardCache: vi.fn(),
}));

vi.mock("../lib/webhook-dispatcher.js", () => ({
  dispatchWebhooks: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../lib/logger.js", () => ({
  logger: {
    error: vi.fn(),
    warn: vi.fn(),
    info: vi.fn(),
    debug: vi.fn(),
  },
}));

const createCaller = createCallerFactory(projectRouter);

beforeEach(() => {
  vi.clearAllMocks();
});

function createManagerCaller(db: Record<string, unknown>) {
  return createCaller({
    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,
    },
  });
}

function createUserCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "user@example.com", name: "User", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_1",
      systemRole: SystemRole.USER,
      permissionOverrides: null,
    },
  });
}

function createUnauthenticatedCaller(db: Record<string, unknown>) {
  return createCaller({
    session: null,
    db: db as never,
    dbUser: null,
  });
}

const VALID_CREATE_INPUT = {
  shortCode: "PROJ-001",
  name: "Test Project",
  orderType: "CHARGEABLE" as const,
  allocationType: "INT" as const,
  winProbability: 100,
  budgetCents: 500000,
  startDate: new Date("2026-06-01"),
  endDate: new Date("2026-12-31"),
  status: "ACTIVE" as const,
  responsiblePerson: "Jane Doe",
  staffingReqs: [],
  dynamicFields: {},
};

function mockDbForCreate(overrides: Record<string, unknown> = {}) {
  return {
    project: {
      findUnique: vi.fn().mockResolvedValue(null),
      create: vi.fn().mockResolvedValue({
        id: "proj_new",
        ...VALID_CREATE_INPUT,
      }),
    },
    blueprint: {
      findUnique: vi.fn().mockResolvedValue(null),
      findMany: vi.fn().mockResolvedValue([]),
    },
    auditLog: {
      create: vi.fn().mockResolvedValue({}),
    },
    $transaction: vi.fn(async (fn: (tx: unknown) => unknown) => {
      const tx = {
        project: {
          create: vi.fn().mockResolvedValue({ id: "proj_new", ...VALID_CREATE_INPUT }),
          update: vi.fn().mockResolvedValue({ id: "proj_1", ...VALID_CREATE_INPUT }),
        },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
      };
      return fn(tx);
    }),
    ...overrides,
  };
}

describe("project create", () => {
  it("rejects unauthenticated requests", async () => {
    const caller = createUnauthenticatedCaller(mockDbForCreate());
    await expect(caller.create(VALID_CREATE_INPUT)).rejects.toMatchObject({
      code: "UNAUTHORIZED",
    });
  });

  it("rejects non-manager users", async () => {
    const caller = createUserCaller(mockDbForCreate());
    await expect(caller.create(VALID_CREATE_INPUT)).rejects.toMatchObject({
      code: "FORBIDDEN",
    });
  });

  it("rejects duplicate short codes", async () => {
    const db = mockDbForCreate({
      project: {
        findUnique: vi.fn().mockResolvedValue({ id: "existing", shortCode: "PROJ-001" }),
        create: vi.fn(),
      },
    });
    const caller = createManagerCaller(db);

    await expect(caller.create(VALID_CREATE_INPUT)).rejects.toMatchObject({
      code: "CONFLICT",
    });
  });

  it("creates project with audit log for managers", async () => {
    const db = mockDbForCreate();
    const caller = createManagerCaller(db);

    const result = await caller.create(VALID_CREATE_INPUT);
    expect(result).toMatchObject({ id: "proj_new" });

    // Verify transaction was called (audit log + project creation)
    expect(db.$transaction).toHaveBeenCalled();
  });

  it("rejects invalid budget (negative cents)", async () => {
    const db = mockDbForCreate();
    const caller = createManagerCaller(db);

    await expect(caller.create({ ...VALID_CREATE_INPUT, budgetCents: -100 })).rejects.toThrow();
  });
});

describe("project update", () => {
  it("rejects unauthenticated requests", async () => {
    const db = mockDbForCreate();
    const caller = createUnauthenticatedCaller(db);

    await expect(caller.update({ id: "proj_1", data: { name: "Updated" } })).rejects.toMatchObject({
      code: "UNAUTHORIZED",
    });
  });

  it("rejects non-manager users", async () => {
    const db = mockDbForCreate();
    const caller = createUserCaller(db);

    await expect(caller.update({ id: "proj_1", data: { name: "Updated" } })).rejects.toMatchObject({
      code: "FORBIDDEN",
    });
  });

  it("throws NOT_FOUND for non-existent project", async () => {
    const db = mockDbForCreate({
      project: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
    });
    const caller = createManagerCaller(db);

    await expect(
      caller.update({ id: "proj_missing", data: { name: "Updated" } }),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });

  it("updates project and creates audit log", async () => {
    const existing = {
      id: "proj_1",
      ...VALID_CREATE_INPUT,
      blueprintId: null,
      dynamicFields: {},
    };
    const db = mockDbForCreate({
      project: {
        findUnique: vi.fn().mockResolvedValue(existing),
      },
    });
    const caller = createManagerCaller(db);

    const result = await caller.update({
      id: "proj_1",
      data: { name: "Renamed Project" },
    });

    expect(result).toMatchObject({ id: "proj_1" });
    expect(db.$transaction).toHaveBeenCalled();
  });

  it("allows partial updates (only budget)", async () => {
    const existing = {
      id: "proj_1",
      ...VALID_CREATE_INPUT,
      blueprintId: null,
      dynamicFields: {},
    };
    const db = mockDbForCreate({
      project: {
        findUnique: vi.fn().mockResolvedValue(existing),
      },
    });
    const caller = createManagerCaller(db);

    const result = await caller.update({
      id: "proj_1",
      data: { budgetCents: 1000000 },
    });

    expect(result).toBeDefined();
  });
});
