import { describe, expect, it, beforeAll, afterAll } from "vitest";
import type { Server } from "http";
import type { Express } from "express";
import type { Scenario } from "../../../shared/types/game";
import type { LeaderboardEntry } from "../../../shared/types/leaderboard";
import { VIEWER_SESSION_COOKIE } from "../../../shared/auth/constants";
import { createViewerSessionToken } from "../../../shared/auth/session";
import {
  getBackendUrl,
  isExternalTarget,
  startLocalServer,
  postChatSSE,
  getExpectedScenarioTrafficSource,
  getScenarioRequestHeaders,
} from "./helpers";

interface ScenarioResponse {
  scenario: Scenario;
  sessionToken: string;
}

interface CommandResponse {
  output: string;
  exitCode: number;
}

interface LeaderboardResponse {
  entries: LeaderboardEntry[];
  hallOfFame: unknown[];
}

interface ErrorResponse {
  error: string;
}

let baseUrl: string;
let localServer: Server | null = null;
let savedMockMode: string | undefined;
let savedTurnstileSecret: string | undefined;
let savedAuthSecret: string | undefined;
let savedAntiAbuseSecret: string | undefined;
let savedPersistentLeaderboardEnabled: string | undefined;

function buildGithubAuthCookie(): string {
  return `${VIEWER_SESSION_COOKIE}=${createViewerSessionToken(
    {
      kind: "github",
      githubUserId: "12345",
      githubLogin: "octocat",
      displayName: "The Octocat",
      avatarUrl: null,
      issuedAt: Date.now(),
      expiresAt: Date.now() + 60_000,
    },
    "test-secret"
  )}`;
}

async function createFullApp(): Promise<Express> {
  savedMockMode = process.env.AI_MOCK_MODE;
  savedTurnstileSecret = process.env.TURNSTILE_SECRET_KEY;
  savedAuthSecret = process.env.AUTH_SESSION_SECRET;
  savedAntiAbuseSecret = process.env.ANTI_ABUSE_HMAC_SECRET;
  savedPersistentLeaderboardEnabled = process.env.PERSISTENT_LEADERBOARD_ENABLED;
  process.env.AI_MOCK_MODE = "true";
  process.env.TURNSTILE_SECRET_KEY = "test-secret";
  process.env.AUTH_SESSION_SECRET = "test-secret";
  process.env.ANTI_ABUSE_HMAC_SECRET = "test-hmac";
  process.env.PERSISTENT_LEADERBOARD_ENABLED = "true";
  const { initStorage } = await import("../lib/storage");
  await initStorage();
  const { default: express } = await import("express");
  const { default: cors } = await import("cors");
  const { chatRouter } = await import("../routes/chat");
  const { commandRouter } = await import("../routes/command");
  const { scenarioRouter } = await import("../routes/scenario");
  const { gameplayRouter } = await import("../routes/gameplay");
  const { scoresRouter } = await import("../routes/scores");
  const { healthRouter } = await import("../routes/health");
  const { guideRouter } = await import("../routes/guide");

  const app = express();
  app.use(cors());
  app.use(express.json());
  app.use("/api/chat", chatRouter);
  app.use("/api/command", commandRouter);
  app.use("/api/scenario", scenarioRouter);
  app.use("/api/gameplay", gameplayRouter);
  app.use("/api/scores", scoresRouter);
  app.use("/api/guide", guideRouter);
  app.use("/", healthRouter);
  return app;
}

beforeAll(async () => {
  if (isExternalTarget()) {
    baseUrl = getBackendUrl();
    return;
  }
  const app = await createFullApp();
  const result = await startLocalServer(app);
  baseUrl = result.url;
  localServer = result.server;
}, 120000);

afterAll(() => {
  if (localServer) {
    localServer.close();
    localServer = null;
  }
  if (savedMockMode === undefined) {
    delete process.env.AI_MOCK_MODE;
  } else {
    process.env.AI_MOCK_MODE = savedMockMode;
  }
  if (savedTurnstileSecret === undefined) {
    delete process.env.TURNSTILE_SECRET_KEY;
  } else {
    process.env.TURNSTILE_SECRET_KEY = savedTurnstileSecret;
  }
  if (savedAuthSecret === undefined) {
    delete process.env.AUTH_SESSION_SECRET;
  } else {
    process.env.AUTH_SESSION_SECRET = savedAuthSecret;
  }
  if (savedAntiAbuseSecret === undefined) {
    delete process.env.ANTI_ABUSE_HMAC_SECRET;
  } else {
    process.env.ANTI_ABUSE_HMAC_SECRET = savedAntiAbuseSecret;
  }
  if (savedPersistentLeaderboardEnabled === undefined) {
    delete process.env.PERSISTENT_LEADERBOARD_ENABLED;
  } else {
    process.env.PERSISTENT_LEADERBOARD_ENABLED = savedPersistentLeaderboardEnabled;
  }
});

describe("health endpoints", () => {
  it("GET /healthz returns ok", async () => {
    const res = await fetch(`${baseUrl}/healthz`);
    expect(res.status).toBe(200);
    const body = (await res.json()) as { status: string };
    expect(body.status).toBe("ok");
  });

  it("GET /readyz returns ready in mock mode", async () => {
    const res = await fetch(`${baseUrl}/readyz`);
    expect(res.status).toBe(200);
    const body = (await res.json()) as { status: string };
    expect(body.status).toBe("ready");
  });
});

describe("full game flow: scenario -> chat -> command -> scores", () => {
  let scenario: Scenario;
  let sessionToken: string;

  it("POST /api/scenario creates a scenario and session token", async () => {
    const res = await fetch(`${baseUrl}/api/scenario`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        cookie: buildGithubAuthCookie(),
        ...getScenarioRequestHeaders(),
      },
      body: JSON.stringify({ difficulty: "easy" }),
    });
    expect(res.status).toBe(200);

    const body = (await res.json()) as ScenarioResponse;
    expect(body.scenario).toBeDefined();
    expect(body.sessionToken).toBeDefined();
    expect(typeof body.sessionToken).toBe("string");

    scenario = body.scenario;
    sessionToken = body.sessionToken;

    expect(scenario.id).toBeDefined();
    expect(scenario.title).toBeDefined();
    expect(scenario.difficulty).toBe("easy");
    expect(scenario.incidentTicket).toBeDefined();
    expect(scenario.clusterContext).toBeDefined();
    expect(scenario.clusterContext.alerts).toBeInstanceOf(Array);
  });

  it("POST /api/chat responds with SSE stream", async () => {
    const result = await postChatSSE(baseUrl, {
      sessionToken,
      messages: [
        { role: "user", content: "What do I see in the incident ticket?" },
      ],
      scenario,
      currentPhase: "reading",
    });
    expect(result.status).toBe(200);
    expect(result.done).toBe(true);
    expect(result.chunks.length).toBeGreaterThan(0);

    for (const chunk of result.chunks) {
      const parsed = JSON.parse(chunk) as Record<string, unknown>;
      expect(
        "text" in parsed || "reasoning" in parsed || "error" in parsed,
      ).toBe(true);
    }
  });

  it("POST /api/chat with follow-up preserves conversation", async () => {
    const result = await postChatSSE(baseUrl, {
      sessionToken,
      messages: [
        { role: "user", content: "What do I see in the incident ticket?" },
        {
          role: "assistant",
          content: "The ticket shows a cluster issue. [PHASE:reading]",
        },
        {
          role: "user",
          content: "Let me check the Geneva dashboard for cluster history.",
        },
      ],
      scenario,
      currentPhase: "context",
    });
    expect(result.status).toBe(200);
    expect(result.done).toBe(true);
  });

  it("POST /api/command simulates oc command output", async () => {
    const res = await fetch(`${baseUrl}/api/command`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sessionToken,
        command: "oc get nodes",
        type: "oc",
        scenario,
      }),
    });
    expect(res.status).toBe(200);

    const body = (await res.json()) as CommandResponse;
    expect(body.output).toBeDefined();
    expect(typeof body.output).toBe("string");
    expect(body.output.length).toBeGreaterThan(0);
    expect(body.exitCode).toBe(0);
  });

  it("POST /api/command simulates kql query output", async () => {
    const res = await fetch(`${baseUrl}/api/command`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sessionToken,
        command:
          'ClusterAuditLogs | where Verb == "delete" | project TimeGenerated, User, ObjectRef',
        type: "kql",
        scenario,
      }),
    });
    expect(res.status).toBe(200);

    const body = (await res.json()) as CommandResponse;
    expect(body.output).toBeDefined();
    expect(body.exitCode).toBe(0);
  });

  it("GET /api/scores returns leaderboard", async () => {
    const res = await fetch(`${baseUrl}/api/scores`);
    expect(res.status).toBe(200);

    const body = (await res.json()) as LeaderboardResponse;
    expect(body.entries).toBeInstanceOf(Array);
    expect(body.hallOfFame).toBeInstanceOf(Array);
  });

  it("POST /api/scores submits score with valid session token", async () => {
    const telemetryRes = await fetch(`${baseUrl}/api/gameplay`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sessionToken,
        lifecycleState: "completed",
        commandCount: 8,
        durationMs: 90_000,
        scoringEvents: [
          { type: "bonus", dimension: "efficiency", points: 20, reason: "eff", timestamp: Date.now() },
          { type: "bonus", dimension: "safety", points: 22, reason: "safe", timestamp: Date.now() },
          { type: "bonus", dimension: "documentation", points: 18, reason: "docs", timestamp: Date.now() },
          { type: "bonus", dimension: "accuracy", points: 15, reason: "acc", timestamp: Date.now() },
        ],
      }),
    });
    expect(telemetryRes.status).toBe(202);

    const res = await fetch(`${baseUrl}/api/scores`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sessionToken,
      }),
    });
    expect(res.status).toBe(201);

    const body = (await res.json()) as LeaderboardEntry;
    expect(body.id).toBeDefined();
    expect(body.nickname).toBe("octocat");
    expect(body.difficulty).toBe("easy");
    expect(body.score.total).toBe(75);
    expect(body.trafficSource).toBe(getExpectedScenarioTrafficSource());
  });

  it("POST /api/scores rejects reuse of consumed session token", async () => {
    const res = await fetch(`${baseUrl}/api/scores`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sessionToken,
        nickname: "CheatSRE",
      }),
    });
    expect(res.status).toBe(403);

    const body = (await res.json()) as ErrorResponse;
    expect(body.error).toContain("Invalid or already used");
  });
});

describe("scenario validation", () => {
  it("rejects invalid difficulty", async () => {
    const res = await fetch(`${baseUrl}/api/scenario`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ difficulty: "impossible" }),
    });
    expect(res.status).toBe(400);
  });

  for (const difficulty of ["easy", "medium", "hard"] as const) {
    it(`generates ${difficulty} scenario`, async () => {
      const res = await fetch(`${baseUrl}/api/scenario`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          cookie: buildGithubAuthCookie(),
        },
        body: JSON.stringify({ difficulty }),
      });
      expect(res.status).toBe(200);

      const body = (await res.json()) as ScenarioResponse;
      expect(body.scenario.difficulty).toBe(difficulty);
      expect(body.sessionToken).toBeDefined();
    });
  }
});

describe("command validation", () => {
  it("rejects invalid command type", async () => {
    const res = await fetch(`${baseUrl}/api/command`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sessionToken: "session-invalid",
        command: "something",
        type: "invalid",
        scenario: null,
      }),
    });
    expect(res.status).toBe(400);
  });
});

describe("guide endpoint", () => {
  it("GET /api/guide returns guide content", async () => {
    const res = await fetch(`${baseUrl}/api/guide`);
    expect(res.status).toBe(200);

    const body = (await res.json()) as Record<string, unknown>;
    expect(body).toBeDefined();
  });
});
