import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  RoleNotFoundError,
  RoleNotActiveError,
  MissingRequiredFieldError,
  RoleAlreadyExistsError,
  InvalidPermissionError,
} from "../lib/errors.generated";
import { baseRole, adminRole, inactiveRole } from "../testing/fixtures";
import { run } from "./updateRole";

describe("updateRole", () => {
  // Error cases
  it("throws when role does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "nonexistent-role", name: "New Name" }),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(RoleNotFoundError);
    }
  });

  it("throws when role is INACTIVE", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([inactiveRole]);

    const result = await mock.withTx((trx) => run(trx, { id: inactiveRole.id, name: "New Name" }));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(RoleNotActiveError);
    }
  });

  it("throws when new name already exists", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    // First select: find the role being updated
    // Second select: find a conflicting role with the same name
    mock.enqueueResults([baseRole], [adminRole]);

    const result = await mock.withTx((trx) => run(trx, { id: baseRole.id, name: adminRole.name }));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(RoleAlreadyExistsError);
    }
  });

  it("throws when new name is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseRole]);

    const result = await mock.withTx((trx) => run(trx, { id: baseRole.id, name: "" }));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MissingRequiredFieldError);
    }
  });

  // Success cases
  it("updates role name successfully", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updatedRole = { ...baseRole, name: "Updated Manager" };

    // First select: find the role; second select: no conflicting name
    mock.enqueueResults([baseRole], [], [updatedRole]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: baseRole.id, name: "Updated Manager" }),
    );
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.role.name).toBe("Updated Manager");
    }
    expect(mock.updates.length).toBeGreaterThan(0);
  });

  it("updates role description successfully", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updatedRole = { ...baseRole, description: "Updated description" };

    mock.enqueueResults([baseRole], [updatedRole]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: baseRole.id, description: "Updated description" }),
    );
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.role.description).toBe("Updated description");
    }
    expect(mock.updates.length).toBeGreaterThan(0);
  });

  it("clears role description when null is passed", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updatedRole = { ...baseRole, description: null };

    mock.enqueueResults([baseRole], [updatedRole]);

    const result = await mock.withTx((trx) => run(trx, { id: baseRole.id, description: null }));
    expect(result.ok).toBe(true);
    const setCall = mock.updates[0].updateValues();
    expect(setCall.description).toBeNull();
  });

  it("does not touch description when undefined is passed", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    mock.enqueueResults([baseRole], [], [baseRole]);

    const result = await mock.withTx((trx) => run(trx, { id: baseRole.id, name: "Renamed" }));
    expect(result.ok).toBe(true);
    const setCall = mock.updates[0].updateValues();
    expect(setCall).not.toHaveProperty("description");
  });

  it("updates role permissions", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const newPermissions = ["sales:salesOrder:create", "inventory:stockOperation:approve"];
    const updatedRole = { ...baseRole, permissions: newPermissions };

    mock.enqueueResults([baseRole], [updatedRole]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: baseRole.id, permissions: newPermissions }),
    );
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.role.permissions).toEqual(newPermissions);
    }
    expect(mock.updates.length).toBeGreaterThan(0);
  });

  it("throws when permissions contain invalid format", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseRole]);

    const result = await mock.withTx((trx) =>
      run(trx, {
        id: baseRole.id,
        permissions: ["valid:scope:command", "1invalid"],
      }),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidPermissionError);
    }
  });

  it("does not affect existing UserRole or RolePermission assignments", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updatedRole = { ...baseRole, name: "Renamed Role" };

    mock.enqueueResults([baseRole], [], [updatedRole]);

    const result = await mock.withTx((trx) => run(trx, { id: baseRole.id, name: "Renamed Role" }));
    expect(result.ok).toBe(true);
    expect(mock.inserts).toHaveLength(0);
    expect(mock.deletes).toHaveLength(0);
  });
});
