import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import { Transaction } from "../generated/kysely-tailordb";
import { RoleNotFoundError, UserNotActiveError, UserNotFoundError } from "../lib/errors.generated";
import { activeUser, baseRole, baseUserRole, inactiveUser, pendingUser } from "../testing/fixtures";
import { run } from "./assignRoleToUser";

describe("assignRoleToUser", () => {
  it("throws when user does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValue(undefined);

    const result = await run(db, {
      userId: "nonexistent-user",
      roleId: baseRole.id,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(UserNotFoundError);
    }
  });

  it("throws when role does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(activeUser).mockReturnValueOnce(undefined);

    const result = await run(db, {
      userId: activeUser.id,
      roleId: "nonexistent-role",
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(RoleNotFoundError);
    }
  });

  it("throws when user is PENDING", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(pendingUser).mockReturnValueOnce(baseRole);

    const result = await run(db, {
      userId: pendingUser.id,
      roleId: baseRole.id,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(UserNotActiveError);
    }
  });

  it("throws when user is INACTIVE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(inactiveUser).mockReturnValueOnce(baseRole);

    const result = await run(db, {
      userId: inactiveUser.id,
      roleId: baseRole.id,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(UserNotActiveError);
    }
  });

  it("returns success when assignment already exists (idempotent)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedUser = { ...activeUser, permissions: ["orders:read"] };
    spies.select
      .mockReturnValueOnce(activeUser)
      .mockReturnValueOnce(baseRole)
      .mockReturnValueOnce(baseUserRole) // Assignment exists
      .mockReturnValueOnce([{ key: "orders:read" }]); // Permission keys from recompute
    spies.update.mockReturnValueOnce(updatedUser);

    const result = await run(db, {
      userId: activeUser.id,
      roleId: baseRole.id,
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.userRole).toEqual(baseUserRole);
      expect(result.value.user.permissions).toEqual(["orders:read"]);
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("creates UserRole and recomputes permissions", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const newUserRole = {
      ...baseUserRole,
      id: "new-user-role-id",
    };
    const updatedUser = { ...activeUser, permissions: ["orders:read"] };

    spies.select
      .mockReturnValueOnce(activeUser)
      .mockReturnValueOnce(baseRole)
      .mockReturnValueOnce(undefined) // No existing assignment
      .mockReturnValueOnce([{ key: "orders:read" }]); // Permission keys from recompute
    spies.insert.mockReturnValue(newUserRole);
    spies.update.mockReturnValueOnce(updatedUser);

    const result = await run(db, {
      userId: activeUser.id,
      roleId: baseRole.id,
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.userRole.userId).toBe(activeUser.id);
      expect(result.value.userRole.roleId).toBe(baseRole.id);
      expect(result.value.user.permissions).toEqual(["orders:read"]);
    }
    expect(spies.insert).toHaveBeenCalled();
  });
});
