import { afterEach, describe, expect, mock, test } from "bun:test";

import type { ResolvedPingTarget } from "../../../../../src/server/checker/runner/icmp/types";
import type { CheckerContext } from "../../../../../src/server/checker/runner/types";

import { IcmpChecker } from "../../../../../src/server/checker/runner/icmp/execute";

const checker = new IcmpChecker("linux");
const originalSpawn = Bun.spawn;

afterEach(() => {
  Bun.spawn = originalSpawn;
  mock.restore();
});

function makeCtx(): CheckerContext {
  return { signal: new AbortController().signal };
}

function makeTarget(overrides?: Partial<ResolvedPingTarget>): ResolvedPingTarget {
  return {
    description: null,
    group: "default",
    icmp: { count: 3, host: "127.0.0.1", packetSize: 56 },
    id: "ping-local",
    intervalMs: 30000,
    name: null,
    timeoutMs: 10000,
    type: "icmp",
    ...overrides,
  };
}

function mockSpawn(stdout: string, exitCode = 0) {
  const calls: string[][] = [];
  const spawnMock = mock((command: string[]) => {
    calls.push(command);
    return {
      exitCode,
      exited: Promise.resolve(exitCode),
      kill: mock(() => undefined),
      stderr: new Response("").body,
      stdout: new Response(stdout).body,
    };
  });
  Bun.spawn = spawnMock as unknown as typeof Bun.spawn;
  return calls;
}

describe("IcmpChecker execute", () => {
  test("执行 ping 并匹配默认 alive", async () => {
    const calls = mockSpawn(`3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.234/2.345/3.456/0.567 ms`);
    const result = await checker.execute(makeTarget(), makeCtx());
    expect(result.matched).toBe(true);
    expect(result.failure).toBeNull();
    expect(result.observation).toMatchObject({
      alive: true,
      avgLatencyMs: 2.345,
      packetLoss: 0,
      received: 3,
      transmitted: 3,
    });
    expect(calls[0]).toContain("ping");
  });

  test("alive 失败短路", async () => {
    mockSpawn(`3 packets transmitted, 0 received, 100% packet loss, time 2003ms`);
    const result = await checker.execute(
      makeTarget({ expect: { alive: true, avgLatencyMs: { lte: 100 } } }),
      makeCtx(),
    );
    expect(result.matched).toBe(false);
    expect(result.failure?.phase).toBe("alive");
    expect(result.observation).toMatchObject({ alive: false, received: 0, transmitted: 3 });
  });

  test("反向 alive 断言通过", async () => {
    mockSpawn(`3 packets transmitted, 0 received, 100% packet loss, time 2003ms`);
    const result = await checker.execute(makeTarget({ expect: { alive: false } }), makeCtx());
    expect(result.matched).toBe(true);
  });

  test("packetLoss 断言失败", async () => {
    mockSpawn(`3 packets transmitted, 2 received, 33% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.234/156.000/340.000/0.567 ms`);
    const result = await checker.execute(
      makeTarget({ expect: { alive: true, packetLossPercent: { lte: 10 } } }),
      makeCtx(),
    );
    expect(result.matched).toBe(false);
    expect(result.failure?.phase).toBe("packetLoss");
    expect(result.observation).toMatchObject({ alive: true, maxLatencyMs: 340 });
  });

  test("解析失败返回结构化错误", async () => {
    mockSpawn("unexpected output");
    const result = await checker.execute(makeTarget(), makeCtx());
    expect(result.matched).toBe(false);
    expect(result.failure).toMatchObject({ kind: "error", path: "parse", phase: "icmp" });
  });

  test("spawn 失败返回 icmp 命令不可用", async () => {
    Bun.spawn = mock(() => {
      throw new Error("ENOENT");
    });
    const result = await checker.execute(makeTarget(), makeCtx());
    expect(result.matched).toBe(false);
    expect(result.failure?.message).toContain("icmp 命令不可用");
    expect(result.observation).toBeNull();
  });

  test("预 abort 返回超时错误", async () => {
    mockSpawn(`3 packets transmitted, 3 received, 0% packet loss, time 2003ms`);
    const controller = new AbortController();
    controller.abort();
    const result = await checker.execute(makeTarget(), { signal: controller.signal });
    expect(result.matched).toBe(false);
    expect(result.failure).toMatchObject({ path: "timeout", phase: "icmp" });
  });
});

describe("IcmpChecker resolve", () => {
  test("解析默认值", () => {
    const target = checker.resolve(
      { icmp: { host: "10.0.0.1" }, id: "ping", type: "icmp" },
      { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
    );
    expect(target.icmp).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 });
    expect(target.group).toBe("default");
    expect(target.rawExpect).toBeUndefined();
    expect(target.expect).toEqual({ alive: true });
  });

  test("serialize 返回摘要和配置", () => {
    const serialized = checker.serialize(makeTarget({ icmp: { count: 5, host: "10.0.0.1", packetSize: 1472 } }));
    expect(serialized.target).toBe("icmp 10.0.0.1");
    expect(JSON.parse(serialized.config)).toEqual({ count: 5, host: "10.0.0.1", packetSize: 1472 });
  });
});
