import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import type {
  DashboardResponse,
  HealthResponse,
  HistoryResponse,
  MetaResponse,
  TargetMetricsResponse,
} from "../../src/shared/api";

import { checkerRegistry } from "../../src/server/checker/runner";
import { CommandChecker } from "../../src/server/checker/runner/cmd/execute";
import { HttpChecker } from "../../src/server/checker/runner/http/execute";
import { ProbeStore } from "../../src/server/checker/store";
import { createNoopLogger } from "../../src/server/logger";
import { startServer } from "../../src/server/server";
import { rmRetry } from "../helpers";

function ensureRegistered() {
  if (!checkerRegistry.supportedTypes.includes("http")) {
    checkerRegistry.register(new HttpChecker());
    checkerRegistry.register(new CommandChecker());
  }
}

beforeAll(() => {
  ensureRegistered();
});

describe("API 路由", () => {
  let tempDir: string;
  let store: ProbeStore;
  let server: ReturnType<typeof startServer>;
  let baseUrl: string;

  beforeAll(async () => {
    tempDir = join(tmpdir(), `dial-api-test-${Date.now()}`);
    await mkdir(tempDir, { recursive: true });
    store = new ProbeStore(join(tempDir, "test.db"));
    store.syncTargets([
      {
        description: null,
        group: "default",
        http: {
          headers: {},
          ignoreSSL: false,
          maxBodyBytes: 104857600,
          maxRedirects: 0,
          method: "GET",
          url: "http://a.com",
        },
        id: "test-a",
        intervalMs: 30000,
        name: "test-a",
        timeoutMs: 10000,
        type: "http",
      },
      {
        cmd: {
          args: ["hello"],
          cwd: "/tmp",
          env: {},
          exec: "echo",
          maxOutputBytes: 104857600,
        },
        description: null,
        group: "default",
        id: "test-b",
        intervalMs: 60000,
        name: "test-b",
        timeoutMs: 5000,
        type: "cmd",
      },
    ]);

    const targets = store.getTargets();
    store.insertCheckResult({
      durationMs: 100,
      failure: null,
      matched: true,
      observation: null,
      targetId: targets[0]!.id,
      timestamp: "2025-01-01T00:00:00.000Z",
    });
    store.insertCheckResult({
      durationMs: null,
      failure: {
        actual: 500,
        expected: 200,
        kind: "error",
        message: "状态码不匹配",
        path: "$.status",
        phase: "status",
      },
      matched: false,
      observation: null,
      targetId: targets[0]!.id,
      timestamp: "2025-01-01T00:10:00.000Z",
    });
    store.insertCheckResult({
      durationMs: null,
      failure: {
        actual: 500,
        expected: 200,
        kind: "error",
        message: "状态码不匹配",
        path: "$.status",
        phase: "status",
      },
      matched: false,
      observation: null,
      targetId: targets[0]!.id,
      timestamp: "2025-01-01T00:20:00.000Z",
    });
    store.insertCheckResult({
      durationMs: 200,
      failure: null,
      matched: true,
      observation: null,
      targetId: targets[0]!.id,
      timestamp: "2025-01-01T00:40:00.000Z",
    });
    store.insertCheckResult({
      durationMs: 400,
      failure: null,
      matched: true,
      observation: null,
      targetId: targets[0]!.id,
      timestamp: "2025-01-01T01:10:00.000Z",
    });

    const now = Date.now();
    store.insertCheckResult({
      durationMs: 120,
      failure: null,
      matched: true,
      observation: null,
      targetId: targets[0]!.id,
      timestamp: new Date(now - 90 * 60 * 1000).toISOString(),
    });
    store.insertCheckResult({
      durationMs: null,
      failure: {
        actual: 500,
        expected: 200,
        kind: "error",
        message: "状态码不匹配",
        path: "$.status",
        phase: "status",
      },
      matched: false,
      observation: null,
      targetId: targets[0]!.id,
      timestamp: new Date(now - 60 * 60 * 1000).toISOString(),
    });
    store.insertCheckResult({
      durationMs: null,
      failure: {
        actual: 500,
        expected: 200,
        kind: "error",
        message: "状态码不匹配",
        path: "$.status",
        phase: "status",
      },
      matched: false,
      observation: null,
      targetId: targets[0]!.id,
      timestamp: new Date(now - 30 * 60 * 1000).toISOString(),
    });

    server = startServer({
      config: { host: "127.0.0.1", port: 0 },
      logger: createNoopLogger(),
      mode: "test",
      store,
      version: "0.1.0",
    });
    baseUrl = `http://127.0.0.1:${server.port}`;
  });

  afterAll(async () => {
    await server.stop(true);
    store.close();
    await rmRetry(tempDir);
  });

  test("/health 返回健康检查", async () => {
    const response = await fetch(`${baseUrl}/health`);
    const body = (await response.json()) as HealthResponse;

    expect(response.status).toBe(200);
    expect(body.ok).toBe(true);
    expect(body.service).toBe("dial-server");
  });

  test("/api/dashboard 返回总览和目标列表", async () => {
    const response = await fetch(`${baseUrl}/api/dashboard?window=24h&recentLimit=2`);
    const body = (await response.json()) as DashboardResponse;

    expect(response.status).toBe(200);
    expect(body.summary.total).toBe(2);
    expect(body.summary.up).toBe(0);
    expect(body.summary.down).toBe(2);
    expect(body.summary.incidents).toBe(1);
    expect(body.summary.lastCheckTime).not.toBeNull();
    expect(body.summary.window.label).toBe("24h");
    expect(body.targets).toHaveLength(2);

    const tA = body.targets.find((t) => t.name === "test-a")!;
    expect(tA.id).toBe("test-a");
    expect(tA.type).toBe("http");
    expect(tA.target).toBe("http://a.com");
    expect(tA.group).toBe("default");
    expect(tA.latestCheck).not.toBeNull();
    expect(tA.latestCheck!.matched).toBe(false);
    expect(tA.latestCheck!.failure).not.toBeNull();
    expect(tA.recentSamples).toHaveLength(2);
    expect(tA.stats).toMatchObject({ availability: 33.33, downChecks: 2, totalChecks: 3, upChecks: 1 });
    expect(tA.currentStreak).toEqual({ capped: true, count: 2, up: false });

    const tB = body.targets.find((t) => t.name === "test-b")!;
    expect(tB.type).toBe("cmd");
    expect(tB.target).toBe("exec echo hello");
    expect(tB.latestCheck).toBeNull();
    expect(tB.stats).toMatchObject({ availability: 0, downChecks: 0, totalChecks: 0, upChecks: 0 });
    expect(tB.currentStreak).toBeNull();
  });

  test("dashboard 无效参数返回 400", async () => {
    const invalidWindow = await fetch(`${baseUrl}/api/dashboard?window=7d`);
    const invalidLimit = await fetch(`${baseUrl}/api/dashboard?recentLimit=0`);

    expect(invalidWindow.status).toBe(400);
    expect(invalidLimit.status).toBe(400);
  });

  test("/api/meta 返回 checker 类型列表和版本号", async () => {
    const response = await fetch(`${baseUrl}/api/meta`);
    const body = (await response.json()) as MetaResponse;

    expect(response.status).toBe(200);
    expect(body.checkerTypes).toEqual(checkerRegistry.supportedTypes);
    expect(body.checkerTypes).toContain("http");
    expect(body.checkerTypes).toContain("cmd");
    expect(body.version).toBe("0.1.0");
  });

  test("不支持的 method 在有 API 通配符时返回 404", async () => {
    const response = await fetch(`${baseUrl}/api/dashboard`, { method: "POST" });
    expect(response.status).toBe(404);
  });

  test("/api/targets/:id/history 返回历史记录", async () => {
    const targets = store.getTargets();
    const from = "2024-01-01T00:00:00.000Z";
    const to = "2025-01-02T00:00:00.000Z";
    const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}`);
    const body = (await response.json()) as HistoryResponse;

    expect(response.status).toBe(200);
    expect(body.items).toHaveLength(5);
    expect(body.total).toBe(5);
    expect(body.page).toBe(1);
    expect(body.pageSize).toBe(20);
    const failedItem = body.items.find((item) => item.failure);
    expect(failedItem?.failure?.kind).toBe("error");
  });

  test("/api/targets/:id/history 支持 page 参数", async () => {
    const targets = store.getTargets();
    const from = "2024-01-01T00:00:00.000Z";
    const to = "2025-01-02T00:00:00.000Z";
    const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}&pageSize=1`);
    const body = (await response.json()) as HistoryResponse;

    expect(response.status).toBe(200);
    expect(body.items).toHaveLength(1);
    expect(body.total).toBe(5);
  });

  test("history pageSize 超过上限返回 400", async () => {
    const targets = store.getTargets();
    const from = "2024-01-01T00:00:00.000Z";
    const to = "2026-12-31T23:59:59.999Z";
    const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}&pageSize=201`);
    const body = (await response.json()) as Record<string, unknown>;

    expect(response.status).toBe(400);
    expect(body["error"]).toBe("pageSize must not exceed 200");
  });

  test("/api/targets/:id/metrics 返回单目标统计和趋势", async () => {
    const targets = store.getTargets();
    const from = "2025-01-01T00:00:00.000Z";
    const to = "2025-01-01T01:59:59.999Z";
    const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/metrics?from=${from}&to=${to}&bucket=1h`);
    const body = (await response.json()) as TargetMetricsResponse;

    expect(response.status).toBe(200);
    expect(body.targetId).toBe(targets[0]!.id);
    expect(body.window.bucket).toBe("1h");
    expect(body.stats).toMatchObject({
      availability: 60,
      avgDurationMs: 233.33,
      downChecks: 2,
      incidentCount: 1,
      longestOutage: 30 * 60 * 1000,
      mttr: 30 * 60 * 1000,
      p95DurationMs: 400,
      p99DurationMs: 400,
      totalChecks: 5,
      upChecks: 3,
    });
    expect(body.stats.currentStreak).toEqual({ count: 2, up: true });
    expect(body.trend[0]).toMatchObject({
      availability: 50,
      avgDurationMs: 150,
      bucketStart: "2025-01-01T00:00:00.000Z",
      downChecks: 2,
      maxDurationMs: 200,
      minDurationMs: 100,
      totalChecks: 4,
      upChecks: 2,
    });
  });

  test("/api/targets/:id/metrics 无数据返回空指标", async () => {
    const targets = store.getTargets();
    const target = targets.find((item) => item.name === "test-b")!;
    const response = await fetch(
      `${baseUrl}/api/targets/${target.id}/metrics?from=2025-01-01T00:00:00.000Z&to=2025-01-01T01:00:00.000Z`,
    );
    const body = (await response.json()) as TargetMetricsResponse;

    expect(response.status).toBe(200);
    expect(body.stats).toEqual({
      availability: 0,
      avgDurationMs: null,
      currentStreak: null,
      downChecks: 0,
      incidentCount: 0,
      longestOutage: null,
      mttr: null,
      p95DurationMs: null,
      p99DurationMs: null,
      totalChecks: 0,
      upChecks: 0,
    });
    expect(body.trend).toEqual([]);
  });

  test("查询不存在的目标返回 404", async () => {
    const response = await fetch(
      `${baseUrl}/api/targets/99999/history?from=2024-01-01T00:00:00.000Z&to=2026-12-31T23:59:59.999Z`,
    );
    const body = (await response.json()) as Record<string, unknown>;

    expect(response.status).toBe(404);
    expect(body["error"]).toBe("Target not found");
  });

  test("history 缺少 from/to 参数返回 400", async () => {
    const targets = store.getTargets();
    const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/history`);
    const body = (await response.json()) as Record<string, unknown>;

    expect(response.status).toBe(400);
    expect(body["error"]).toContain("from and to");
  });

  test("metrics 缺少 from/to 参数返回 400", async () => {
    const targets = store.getTargets();
    const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/metrics`);
    const body = (await response.json()) as Record<string, unknown>;

    expect(response.status).toBe(400);
    expect(body["error"]).toContain("from and to");
  });

  test("metrics 无效 target id 返回 400", async () => {
    const response = await fetch(
      `${baseUrl}/api/targets/_invalid/metrics?from=2024-01-01T00:00:00Z&to=2024-01-02T00:00:00Z`,
    );
    const body = (await response.json()) as Record<string, unknown>;

    expect(response.status).toBe(400);
    expect(body["error"]).toBe("Invalid target ID");
  });

  test("metrics 无效 bucket 和不存在目标返回错误", async () => {
    const targets = store.getTargets();
    const invalidBucket = await fetch(
      `${baseUrl}/api/targets/${targets[0]!.id}/metrics?from=2025-01-01T00:00:00.000Z&to=2025-01-01T01:00:00.000Z&bucket=5m`,
    );
    const missingTarget = await fetch(
      `${baseUrl}/api/targets/99999/metrics?from=2025-01-01T00:00:00.000Z&to=2025-01-01T01:00:00.000Z`,
    );

    expect(invalidBucket.status).toBe(400);
    expect(missingTarget.status).toBe(404);
  });

  test("未知 /api/* 返回 404", async () => {
    const response = await fetch(`${baseUrl}/api/missing`);
    expect(response.status).toBe(404);
  });

  test("生产响应包含安全 headers", async () => {
    const prodServer = startServer({
      config: { host: "127.0.0.1", port: 0 },
      logger: createNoopLogger(),
      mode: "production",
      store,
      version: "0.1.0",
    });
    try {
      const response = await fetch(`http://127.0.0.1:${prodServer.port}/api/dashboard`);
      expect(response.headers.get("x-content-type-options")).toBe("nosniff");
      expect(response.headers.get("referrer-policy")).toBe("strict-origin-when-cross-origin");
    } finally {
      await prodServer.stop(true);
    }
  });

  test("损坏的 failure JSON 返回 null 而不崩溃", async () => {
    const targets = store.getTargets();
    const t1Id = targets[0]!.id;

    store.insertCheckResult({
      durationMs: 100,
      failure: { kind: "error", message: "test", path: "$", phase: "body" },
      matched: false,
      observation: null,
      targetId: t1Id,
      timestamp: "2025-06-01T00:00:00.000Z",
    });

    (store as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db
      .prepare("UPDATE check_results SET failure = ? WHERE target_id = ? AND timestamp = ?")
      .run("{invalid json!!!", t1Id, "2025-06-01T00:00:00.000Z");

    const from = "2025-06-01T00:00:00.000Z";
    const to = "2025-06-01T23:59:59.999Z";
    const response = await fetch(`${baseUrl}/api/targets/${t1Id}/history?from=${from}&to=${to}`);
    const body = (await response.json()) as HistoryResponse;

    expect(response.status).toBe(200);
    expect(body.items).toHaveLength(1);
    expect(body.items[0]!.failure).toBeNull();
  });
});
