/**
 * Server Core Tests
 *
 * CORS, 정적 파일 보안, 404, 레지스트리 격리성 검증
 */

import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import {
  startServer,
  createServerRegistry,
  clearDefaultRegistry,
  type ManduServer,
  type ServerRegistry,
} from "../../src/runtime/server";
import type { RoutesManifest } from "../../src/spec/schema";

const baseManifest: RoutesManifest = {
  version: 1,
  routes: [
    { id: "api/health", pattern: "/api/health", kind: "api", module: ".mandu/generated/server/api-health.ts", methods: ["GET"] },
    { id: "api/echo", pattern: "/api/echo", kind: "api", module: ".mandu/generated/server/api-echo.ts", methods: ["GET", "POST"] },
  ],
};

describe("CORS 처리", () => {
  let server: ManduServer | null = null;
  let registry: ServerRegistry;

  beforeEach(() => {
    registry = createServerRegistry();
    registry.registerApiHandler("api/health", async () => Response.json({ ok: true }));
  });

  afterEach(() => {
    if (server) {
      server.stop();
      server = null;
    }
  });

  it("cors: false이면 CORS 헤더가 없다", async () => {
    server = startServer(baseManifest, { port: 0, registry, cors: false });
    const port = server.server.port;

    const res = await fetch(`http://localhost:${port}/api/health`, {
      headers: { Origin: "http://other.com" },
    });

    expect(res.status).toBe(200);
    expect(res.headers.get("access-control-allow-origin")).toBeNull();
  });

  it("cors: true이면 Access-Control-Allow-Origin: *이 붙는다", async () => {
    server = startServer(baseManifest, { port: 0, registry, cors: true });
    const port = server.server.port;

    const res = await fetch(`http://localhost:${port}/api/health`, {
      headers: { Origin: "http://other.com" },
    });

    expect(res.status).toBe(200);
    expect(res.headers.get("access-control-allow-origin")).toBe("*");
  });

  it("OPTIONS preflight 요청에 204 또는 200으로 응답한다", async () => {
    server = startServer(baseManifest, { port: 0, registry, cors: true });
    const port = server.server.port;

    const res = await fetch(`http://localhost:${port}/api/health`, {
      method: "OPTIONS",
      headers: {
        Origin: "http://other.com",
        "Access-Control-Request-Method": "GET",
      },
    });

    expect([200, 204]).toContain(res.status);
    expect(res.headers.get("access-control-allow-methods")).not.toBeNull();
  });
});

describe("404 처리", () => {
  let server: ManduServer | null = null;
  let registry: ServerRegistry;

  beforeEach(() => {
    registry = createServerRegistry();
  });

  afterEach(() => {
    if (server) {
      server.stop();
      server = null;
    }
  });

  it("등록되지 않은 경로에 404를 반환한다", async () => {
    server = startServer(baseManifest, { port: 0, registry });
    const port = server.server.port;

    const res = await fetch(`http://localhost:${port}/not-registered`);

    expect(res.status).toBe(404);
  });

  it("manifest에 없는 API 경로에 404를 반환한다", async () => {
    server = startServer(baseManifest, { port: 0, registry });
    const port = server.server.port;

    const res = await fetch(`http://localhost:${port}/api/does-not-exist`);

    expect(res.status).toBe(404);
  });
});

describe("정적 파일 보안 (Path Traversal 방지)", () => {
  let server: ManduServer | null = null;
  let registry: ServerRegistry;

  beforeEach(() => {
    registry = createServerRegistry();
  });

  afterEach(() => {
    if (server) {
      server.stop();
      server = null;
    }
  });

  it("정적 파일 경로 탈출(backslash 포함)은 403을 반환한다", async () => {
    server = startServer(baseManifest, { port: 0, registry });
    const port = server.server.port;

    const res = await fetch(`http://localhost:${port}/.mandu/client/..%5C..%5C..%5Cetc/passwd`);

    expect(res.status).toBe(403);
  });

  it("null byte 공격을 차단한다", async () => {
    server = startServer(baseManifest, { port: 0, registry });
    const port = server.server.port;

    // URL 인코딩된 null byte
    const res = await fetch(`http://localhost:${port}/.mandu/client/file%00.js`);

    expect(res.status).toBe(400);
  });

  it("잘못된 URL 인코딩은 400을 반환한다", async () => {
    server = startServer(baseManifest, { port: 0, registry });
    const port = server.server.port;

    const res = await fetch(`http://localhost:${port}/.mandu/client/%E0%A4%A`);

    expect(res.status).toBe(400);
  });

  it("존재하지 않는 정적 파일에 404를 반환한다", async () => {
    server = startServer(baseManifest, { port: 0, registry });
    const port = server.server.port;

    const res = await fetch(`http://localhost:${port}/.mandu/client/nonexistent-bundle.js`);

    expect(res.status).toBe(404);
  });
});

describe("ServerRegistry 격리성", () => {
  let servers: ManduServer[] = [];

  afterEach(() => {
    for (const s of servers) {
      try {
        s.stop();
      } catch {
        // 정리 중 에러는 무시 — 나머지 서버는 계속 정리
      }
    }
    servers = [];
    clearDefaultRegistry();
  });

  it("두 개의 격리된 레지스트리는 서로 영향받지 않는다", async () => {
    const registryA = createServerRegistry();
    const registryB = createServerRegistry();

    registryA.registerApiHandler("api/health", async () => Response.json({ server: "A" }));
    registryB.registerApiHandler("api/health", async () => Response.json({ server: "B" }));

    const serverA = startServer(baseManifest, { port: 0, registry: registryA });
    const serverB = startServer(baseManifest, { port: 0, registry: registryB });
    servers.push(serverA, serverB);

    const portA = serverA.server.port;
    const portB = serverB.server.port;

    const resA = await fetch(`http://localhost:${portA}/api/health`);
    const resB = await fetch(`http://localhost:${portB}/api/health`);

    const dataA = await resA.json();
    const dataB = await resB.json();

    expect(dataA.server).toBe("A");
    expect(dataB.server).toBe("B");
  });

  it("registry.clear() 후 핸들러가 제거된다", async () => {
    const registry = createServerRegistry();
    registry.registerApiHandler("api/health", async () => Response.json({ ok: true }));

    const server = startServer(baseManifest, { port: 0, registry });
    servers.push(server);
    const port = server.server.port;

    // 등록 후 정상 응답 확인
    const before = await fetch(`http://localhost:${port}/api/health`);
    expect(before.status).toBe(200);

    // 클리어 후 핸들러 없음 -> runtime misconfiguration
    registry.clear();
    const after = await fetch(`http://localhost:${port}/api/health`);
    expect(after.status).toBe(500);
  });
});

describe("startServer 기본 동작", () => {
  let server: ManduServer | null = null;

  afterEach(() => {
    if (server) {
      server.stop();
      server = null;
    }
    clearDefaultRegistry();
  });

  it("port: 0으로 ephemeral port를 사용한다", () => {
    server = startServer(baseManifest, { port: 0 });

    expect(server.server.port).toBeGreaterThan(0);
    expect(server.server.port).toBeLessThanOrEqual(65535);
  });

  it("ManduServer 객체가 server, router, registry, stop을 포함한다", () => {
    server = startServer(baseManifest, { port: 0 });

    expect(server.server).toBeDefined();
    expect(server.router).toBeDefined();
    expect(server.registry).toBeDefined();
    expect(typeof server.stop).toBe("function");
    expect(typeof server.updateManifest).toBe("function");
  });

  it("updateManifest() refreshes the router dispatch table without restart", async () => {
    const registry = createServerRegistry();
    server = startServer({ version: 1, routes: [] }, { port: 0, registry });
    registry.registerApiHandler("api/live", async () => Response.json({ live: true }));

    const port = server.server.port;
    const before = await fetch(`http://localhost:${port}/api/live`);
    expect(before.status).toBe(404);

    server.updateManifest({
      version: 1,
      routes: [
        {
          id: "api/live",
          pattern: "/api/live",
          kind: "api",
          module: ".mandu/generated/server/api-live.ts",
          methods: ["GET"],
        },
      ],
    });

    const after = await fetch(`http://localhost:${port}/api/live`);
    expect(after.status).toBe(200);
    expect(await after.json()).toEqual({ live: true });
  });

  it("stop() 호출 후 서버 객체를 반환한다", () => {
    server = startServer(baseManifest, { port: 0 });

    // stop()은 에러 없이 실행되어야 함
    expect(() => server!.stop()).not.toThrow();
    server = null;
  });
});

describe("runtime cache control endpoint", () => {
  let server: ManduServer | null = null;
  let registry: ServerRegistry;

  beforeEach(() => {
    registry = createServerRegistry();
  });

  afterEach(() => {
    if (server) {
      server.stop();
      server = null;
    }
  });

  it("rejects requests without the management token", async () => {
    server = startServer(baseManifest, {
      port: 0,
      registry,
      cache: true,
      managementToken: "test-token",
    });

    const port = server.server.port;
    const res = await fetch(`http://localhost:${port}/_mandu/cache`);
    expect(res.status).toBe(401);
  });

  it("returns cache stats and clears entries by path", async () => {
    server = startServer(baseManifest, {
      port: 0,
      registry,
      cache: true,
      managementToken: "test-token",
    });

    const store = server.registry.settings.cacheStore!;
    store.set("home:/products?page=1", {
      html: "<p>products</p>",
      loaderData: {},
      status: 200,
      headers: {},
      createdAt: Date.now(),
      revalidateAfter: Date.now() + 60_000,
      staleUntil: Date.now() + 60_000,
      tags: ["products"],
      maxAgeSeconds: 60,
      swrSeconds: 0,
    });
    store.set("home:/other", {
      html: "<p>other</p>",
      loaderData: {},
      status: 200,
      headers: {},
      createdAt: Date.now(),
      revalidateAfter: Date.now() + 60_000,
      staleUntil: Date.now() + 60_000,
      tags: ["other"],
      maxAgeSeconds: 60,
      swrSeconds: 0,
    });

    const port = server.server.port;
    const statsRes = await fetch(`http://localhost:${port}/_mandu/cache`, {
      headers: { "x-mandu-control-token": "test-token" },
    });
    expect(statsRes.status).toBe(200);
    const statsData = await statsRes.json();
    expect(statsData.enabled).toBe(true);
    expect(statsData.stats.entries).toBe(2);

    const clearRes = await fetch(`http://localhost:${port}/_mandu/cache`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-mandu-control-token": "test-token",
      },
      body: JSON.stringify({ path: "/products" }),
    });
    expect(clearRes.status).toBe(200);
    const clearData = await clearRes.json();
    expect(clearData.target).toBe("path=/products");
    expect(clearData.cleared).toBe(1);
    expect(store.get("home:/products?page=1")).toBeNull();
    expect(store.get("home:/other")).not.toBeNull();
  });
});
