import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import { Transaction } from "../generated/kysely-tailordb";
import {
  InvalidEmailError,
  InvalidNameError,
  MissingRequiredFieldError,
  UserAlreadyExistsError,
  UserNotFoundError,
} from "../lib/errors.generated";
import { activeUser, inactiveUser, pendingUser } from "../testing/fixtures";
import { run } from "./updateUser";

describe("updateUser", () => {
  // Success cases
  it("updates user name", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedUser = { ...activeUser, name: "New Name" };

    spies.select.mockReturnValue(activeUser);
    spies.update.mockReturnValue(updatedUser);

    const result = await run(db, { id: activeUser.id, name: "New Name" });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.user.name).toBe("New Name");
    }
    expect(spies.update).toHaveBeenCalled();
  });

  it("updates user email", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedUser = { ...activeUser, email: "newemail@example.com" };

    // First select: find the user being updated; second select: no conflicting email
    spies.select.mockReturnValueOnce(activeUser);
    spies.select.mockReturnValueOnce(undefined);
    spies.update.mockReturnValue(updatedUser);

    const result = await run(db, { id: activeUser.id, email: "newemail@example.com" });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.user.email).toBe("newemail@example.com");
    }
    expect(spies.update).toHaveBeenCalled();
  });

  it("updates both name and email", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedUser = { ...activeUser, name: "New Name", email: "newemail@example.com" };

    // First select: find the user being updated; second select: no conflicting email
    spies.select.mockReturnValueOnce(activeUser);
    spies.select.mockReturnValueOnce(undefined);
    spies.update.mockReturnValue(updatedUser);

    const result = await run(db, {
      id: activeUser.id,
      name: "New Name",
      email: "newemail@example.com",
    });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.user.name).toBe("New Name");
      expect(result.value.user.email).toBe("newemail@example.com");
    }
    expect(spies.update).toHaveBeenCalled();
  });

  it("updates user in PENDING status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedUser = { ...pendingUser, name: "Updated Pending" };

    spies.select.mockReturnValue(pendingUser);
    spies.update.mockReturnValue(updatedUser);

    const result = await run(db, { id: pendingUser.id, name: "Updated Pending" });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.user.name).toBe("Updated Pending");
    }
  });

  it("updates user in ACTIVE status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedUser = { ...activeUser, name: "Updated Active" };

    spies.select.mockReturnValue(activeUser);
    spies.update.mockReturnValue(updatedUser);

    const result = await run(db, { id: activeUser.id, name: "Updated Active" });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.user.name).toBe("Updated Active");
    }
  });

  it("updates user in INACTIVE status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedUser = { ...inactiveUser, name: "Updated Inactive" };

    spies.select.mockReturnValue(inactiveUser);
    spies.update.mockReturnValue(updatedUser);

    const result = await run(db, { id: inactiveUser.id, name: "Updated Inactive" });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.user.name).toBe("Updated Inactive");
    }
  });

  it("succeeds when email is unchanged (same as current)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedUser = { ...activeUser };

    spies.select.mockReturnValue(activeUser);
    spies.update.mockReturnValue(updatedUser);

    // Email matches current — should skip uniqueness check
    const result = await run(db, { id: activeUser.id, email: activeUser.email });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.user.email).toBe(activeUser.email);
    }
    expect(spies.update).toHaveBeenCalled();
  });

  // Error cases
  it("returns error when user does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValue(undefined);

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

  it("returns error when no fields are provided", async () => {
    const { db } = createMockDb<Transaction>();

    const result = await run(db, { id: "user-active-1" });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MissingRequiredFieldError);
    }
  });

  it("returns error when name is empty", async () => {
    const { db } = createMockDb<Transaction>();

    const result = await run(db, { id: "user-active-1", name: "" });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidNameError);
    }
  });

  it("returns error when name is whitespace only", async () => {
    const { db } = createMockDb<Transaction>();

    const result = await run(db, { id: "user-active-1", name: "   " });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidNameError);
    }
  });

  it("returns error when email format is invalid", async () => {
    const { db } = createMockDb<Transaction>();

    const result = await run(db, { id: "user-active-1", email: "invalidemail" });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidEmailError);
    }
  });

  it("returns error when email is already used by another user", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const anotherUser = { ...activeUser, id: "other-user", email: "taken@example.com" };

    // First select: find the user being updated
    spies.select.mockReturnValueOnce(activeUser);
    // Second select: find the conflicting user with the same email
    spies.select.mockReturnValueOnce(anotherUser);

    const result = await run(db, { id: activeUser.id, email: "taken@example.com" });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(UserAlreadyExistsError);
    }
  });
});
