import { describe, it, expect, vi, beforeEach } from "vitest";
import request from "supertest";
import type { GameSession, AnswerResult } from "@lila/shared";

type SuccessResponse<T> = { success: true; data: T };
type ErrorResponse = { success: false; error: string };
type GameStartResponse = SuccessResponse<GameSession>;
type GameAnswerResponse = SuccessResponse<AnswerResult>;

vi.mock("@lila/db", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@lila/db")>();
  return { ...actual, getGameTerms: vi.fn(), getDistractors: vi.fn() };
});

vi.mock("../lib/auth.js", () => ({
  auth: {
    api: {
      getSession: vi
        .fn()
        .mockResolvedValue({
          session: {
            id: "session-1",
            userId: "user-1",
            token: "fake-token",
            expiresAt: new Date(Date.now() + 1000 * 60 * 60),
            createdAt: new Date(),
            updatedAt: new Date(),
            ipAddress: null,
            userAgent: null,
          },
          user: {
            id: "user-1",
            name: "Test User",
            email: "test@test.com",
            emailVerified: false,
            image: null,
            createdAt: new Date(),
            updatedAt: new Date(),
          },
        }),
    },
    handler: vi.fn(),
  },
}));

vi.mock("better-auth/node", () => ({
  fromNodeHeaders: vi.fn().mockReturnValue({}),
  toNodeHandler: vi.fn().mockReturnValue(vi.fn()),
}));

import { getGameTerms, getDistractors } from "@lila/db";
import { createApp } from "../app.js";

const app = createApp();
const mockGetGameTerms = vi.mocked(getGameTerms);
const mockGetDistractors = vi.mocked(getDistractors);

const validBody = {
  source_language: "en",
  target_language: "it",
  pos: "noun",
  difficulty: "easy",
  rounds: 3,
};

const fakeTerms = [
  { termId: "t1", sourceText: "dog", targetText: "cane", sourceGloss: null },
  { termId: "t2", sourceText: "cat", targetText: "gatto", sourceGloss: null },
  { termId: "t3", sourceText: "house", targetText: "casa", sourceGloss: null },
];

beforeEach(() => {
  vi.clearAllMocks();
  mockGetGameTerms.mockResolvedValue(fakeTerms);
  mockGetDistractors.mockResolvedValue(["wrong1", "wrong2", "wrong3"]);
});

describe("POST /api/v1/game/start", () => {
  it("returns 200 with a valid game session", async () => {
    const res = await request(app).post("/api/v1/game/start").send(validBody);
    const body = res.body as GameStartResponse;
    expect(res.status).toBe(200);
    expect(body.success).toBe(true);
    expect(body.data.sessionId).toBeDefined();
    expect(body.data.questions).toHaveLength(3);
  });

  it("returns 400 when the body is empty", async () => {
    const res = await request(app).post("/api/v1/game/start").send({});
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(400);
    expect(body.success).toBe(false);
    expect(body.error).toBeDefined();
  });

  it("returns 400 when required fields are missing", async () => {
    const res = await request(app)
      .post("/api/v1/game/start")
      .send({ source_language: "en" });
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(400);
    expect(body.success).toBe(false);
  });

  it("returns 400 when a field has an invalid value", async () => {
    const res = await request(app)
      .post("/api/v1/game/start")
      .send({ ...validBody, difficulty: "impossible" });
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(400);
    expect(body.success).toBe(false);
  });

  it("returns 422 when no terms are found for the given filters", async () => {
    mockGetGameTerms.mockResolvedValue([]);

    const res = await request(app).post("/api/v1/game/start").send(validBody);
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(422);
    expect(body.success).toBe(false);
  });

  it("returns a sanitised error message when the body is invalid", async () => {
    const res = await request(app)
      .post("/api/v1/game/start")
      .send({ ...validBody, difficulty: "impossible" });
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(400);
    expect(body.error).toBe("Invalid game settings");
    expect(body.error).not.toContain("Invalid literal value");
    expect(body.error).not.toContain("Invalid enum value");
  });
});

describe("POST /api/v1/game/answer", () => {
  it("returns 200 with an answer result for a valid submission", async () => {
    const startRes = await request(app)
      .post("/api/v1/game/start")
      .send(validBody);
    const startBody = startRes.body as GameStartResponse;
    const { sessionId, questions } = startBody.data;
    const question = questions[0]!;

    const res = await request(app)
      .post("/api/v1/game/answer")
      .send({
        sessionId,
        questionId: question.questionId,
        selectedOptionId: 0,
      });
    const body = res.body as GameAnswerResponse;
    expect(res.status).toBe(200);
    expect(body.success).toBe(true);
    expect(body.data.questionId).toBe(question.questionId);
    expect(typeof body.data.isCorrect).toBe("boolean");
    expect(typeof body.data.correctOptionId).toBe("number");
    expect(body.data.selectedOptionId).toBe(0);
  });

  it("returns 400 when the body is empty", async () => {
    const res = await request(app).post("/api/v1/game/answer").send({});
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(400);
    expect(body.success).toBe(false);
  });

  it("returns 404 when the session does not exist", async () => {
    const res = await request(app)
      .post("/api/v1/game/answer")
      .send({
        sessionId: "00000000-0000-0000-0000-000000000000",
        questionId: "00000000-0000-0000-0000-000000000000",
        selectedOptionId: 0,
      });
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(404);
    expect(body.success).toBe(false);
    expect(body.error).toContain("Game session not found");
  });

  it("returns 409 when the question does not exist in the session", async () => {
    const startRes = await request(app)
      .post("/api/v1/game/start")
      .send(validBody);
    const startBody = startRes.body as GameStartResponse;
    const { sessionId } = startBody.data;

    const res = await request(app)
      .post("/api/v1/game/answer")
      .send({
        sessionId,
        questionId: "00000000-0000-0000-0000-000000000000",
        selectedOptionId: 0,
      });
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(409);
    expect(body.success).toBe(false);
    expect(body.error).toContain("Question already answered");
  });
  
  it("returns 400 when a field has an invalid value", async () => {
    const res = await request(app)
      .post("/api/v1/game/start")
      .send({ ...validBody, difficulty: "impossible" });
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(400);
    expect(body.success).toBe(false);
  });

  it("returns 400 when rounds has an invalid value", async () => {
    const res = await request(app)
      .post("/api/v1/game/start")
      .send({ ...validBody, rounds: "invalid" });
    const body = res.body as ErrorResponse;
    expect(res.status).toBe(400);
    expect(body.success).toBe(false);
  });
});
