import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("@lila/db", () => ({
  createLobby: vi.fn(),
  getLobbyByCodeWithPlayers: vi.fn(),
  addPlayer: vi.fn(),
}));

import {
  createLobby as createLobbyModel,
  getLobbyByCodeWithPlayers,
  addPlayer,
} from "@lila/db";
import { createLobby, joinLobby } from "./lobbyService.js";

const mockCreateLobby = vi.mocked(createLobbyModel);
const mockGetLobbyByCodeWithPlayers = vi.mocked(getLobbyByCodeWithPlayers);
const mockAddPlayer = vi.mocked(addPlayer);

const fakeLobby = {
  id: "00000000-0000-4000-8000-000000000001",
  code: "ABC123",
  hostUserId: "user-1",
  status: "waiting" as const,
  createdAt: new Date(),
};

const fakeLobbyWithPlayers = {
  ...fakeLobby,
  players: [
    {
      lobbyId: fakeLobby.id,
      userId: "user-1",
      score: 0,
      joinedAt: new Date(),
      user: { id: "user-1", name: "Alice" },
    },
  ],
};

beforeEach(() => {
  vi.clearAllMocks();
  mockCreateLobby.mockResolvedValue(fakeLobby);
  mockAddPlayer.mockResolvedValue({
    lobbyId: fakeLobby.id,
    userId: "user-1",
    score: 0,
    joinedAt: new Date(),
  });
  mockGetLobbyByCodeWithPlayers.mockResolvedValue(fakeLobbyWithPlayers);
});

describe("createLobby", () => {
  it("creates a lobby and adds the host as the first player", async () => {
    const result = await createLobby("user-1");

    expect(mockCreateLobby).toHaveBeenCalledOnce();
    expect(mockAddPlayer).toHaveBeenCalledWith(
      fakeLobby.id,
      "user-1",
      expect.any(Number),
    );
    expect(result.id).toBe(fakeLobby.id);
  });

  it("retries on unique code collision", async () => {
    const uniqueViolation = Object.assign(new Error("unique"), {
      code: "23505",
    });
    mockCreateLobby
      .mockRejectedValueOnce(uniqueViolation)
      .mockResolvedValueOnce(fakeLobby);

    const result = await createLobby("user-1");

    expect(mockCreateLobby).toHaveBeenCalledTimes(2);
    expect(result.id).toBe(fakeLobby.id);
  });

  it("throws after max retry attempts", async () => {
    const uniqueViolation = Object.assign(new Error("unique"), {
      code: "23505",
    });
    mockCreateLobby.mockRejectedValue(uniqueViolation);

    await expect(createLobby("user-1")).rejects.toThrow(
      "Could not generate a unique lobby code",
    );
  });

  it("re-throws non-unique-violation errors immediately", async () => {
    const dbError = new Error("connection refused");
    mockCreateLobby.mockRejectedValue(dbError);

    await expect(createLobby("user-1")).rejects.toThrow("connection refused");
    expect(mockCreateLobby).toHaveBeenCalledTimes(1);
  });
});

describe("joinLobby", () => {
  it("returns lobby with players when join succeeds", async () => {
    const fullLobby = {
      ...fakeLobbyWithPlayers,
      players: [
        ...fakeLobbyWithPlayers.players,
        {
          lobbyId: fakeLobby.id,
          userId: "user-2",
          score: 0,
          joinedAt: new Date(),
          user: { id: "user-2", name: "Bob" },
        },
      ],
    };
    mockGetLobbyByCodeWithPlayers
      .mockResolvedValueOnce(fakeLobbyWithPlayers)
      .mockResolvedValueOnce(fullLobby);

    const result = await joinLobby("ABC123", "user-2");

    expect(mockAddPlayer).toHaveBeenCalledWith(
      fakeLobby.id,
      "user-2",
      expect.any(Number),
    );
    expect(result.players).toHaveLength(2);
  });

  it("throws NotFoundError when lobby does not exist", async () => {
    mockGetLobbyByCodeWithPlayers.mockResolvedValue(undefined);

    await expect(joinLobby("XXXXXX", "user-2")).rejects.toThrow(
      "Lobby not found",
    );
  });

  it("throws ConflictError when lobby is not waiting", async () => {
    mockGetLobbyByCodeWithPlayers.mockResolvedValue({
      ...fakeLobbyWithPlayers,
      status: "in_progress" as const,
    });

    await expect(joinLobby("ABC123", "user-2")).rejects.toThrow(
      "Game has already started",
    );
  });

  it("returns lobby idempotently when user already joined", async () => {
    mockGetLobbyByCodeWithPlayers.mockResolvedValue({
      ...fakeLobbyWithPlayers,
      players: [
        {
          lobbyId: fakeLobby.id,
          userId: "user-1",
          score: 0,
          joinedAt: new Date(),
          user: { id: "user-1", name: "Alice" },
        },
      ],
    });

    const result = await joinLobby("ABC123", "user-1");

    expect(mockAddPlayer).not.toHaveBeenCalled();
    expect(result.players).toHaveLength(1);
  });

  it("throws ConflictError when lobby is full", async () => {
    mockGetLobbyByCodeWithPlayers.mockResolvedValue({
      ...fakeLobbyWithPlayers,
      players: Array.from({ length: 4 }, (_, i) => ({
        lobbyId: fakeLobby.id,
        userId: `user-${i}`,
        score: 0,
        joinedAt: new Date(),
        user: { id: `user-${i}`, name: `Player ${i}` },
      })),
    });

    await expect(joinLobby("ABC123", "user-5")).rejects.toThrow(
      "Lobby is full",
    );
  });

  it("throws ConflictError when addPlayer returns falsy (race condition)", async () => {
    mockAddPlayer.mockResolvedValue(undefined);

    await expect(joinLobby("ABC123", "user-2")).rejects.toThrow(
      "Lobby is no longer available",
    );
  });

  it("throws AppError when lobby disappears after addPlayer succeeds", async () => {
    mockGetLobbyByCodeWithPlayers
      .mockResolvedValueOnce(fakeLobbyWithPlayers)
      .mockResolvedValueOnce(undefined);

    await expect(joinLobby("ABC123", "user-2")).rejects.toThrow(
      "Lobby disappeared during join",
    );
  });
});
