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

import type {
  RawUdpExpectConfig,
  ResolvedUdpExpectConfig,
  ResolvedUdpTarget,
} from "../../../../../src/server/checker/runner/udp/types";

import { resolveContentExpectations } from "../../../../../src/server/checker/expect/content";
import { resolveValueExpectation } from "../../../../../src/server/checker/expect/value";
import { UdpChecker } from "../../../../../src/server/checker/runner/udp/execute";

async function createEchoServer(): Promise<{ close: () => void; port: number }> {
  const socket = await Bun.udpSocket({
    socket: {
      data(sock, buf, port, addr) {
        sock.send(buf, port, addr);
      },
      drain() {
        // Bun UDP socket handler 必填项
      },
      error() {
        // Bun UDP socket handler 必填项
      },
    },
  });
  return { close: () => socket.close(), port: socket.port };
}

function makeSignal(timeoutMs: number): { cleanup: () => void; signal: AbortSignal } {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  return { cleanup: () => clearTimeout(timer), signal: controller.signal };
}

function makeTarget(overrides: Partial<ResolvedUdpTarget["udp"]> = {}, raw?: RawUdpExpectConfig): ResolvedUdpTarget {
  const resolvedExpect: ResolvedUdpExpectConfig | undefined = raw
    ? {
        durationMs: resolveValueExpectation(raw.durationMs),
        responded: raw.responded ?? true,
        response: resolveContentExpectations(raw.response),
        responseSize: resolveValueExpectation(raw.responseSize),
        sourceHost: resolveValueExpectation(raw.sourceHost),
        sourcePort: resolveValueExpectation(raw.sourcePort),
      }
    : undefined;

  return {
    description: null,
    expect: resolvedExpect,
    group: "default",
    id: "test-udp",
    intervalMs: 30000,
    name: null,
    rawExpect: raw,
    timeoutMs: 10000,
    type: "udp",
    udp: {
      encoding: "text",
      host: "127.0.0.1",
      maxResponseBytes: 4096,
      payload: "PING",
      port: 0,
      responseEncoding: "text",
      ...overrides,
    },
  };
}

describe("UdpChecker execute", () => {
  it("should resolve and respond successfully", async () => {
    const server = await createEchoServer();
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(true);
      expect(result.failure).toBeNull();
      expect(result.observation).toMatchObject({ responded: true });
      expect(result.durationMs).toBeGreaterThanOrEqual(0);
    } finally {
      server.close();
    }
  });

  it("should work with localhost hostname", async () => {
    const server = await createEchoServer();
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ host: "localhost", port: server.port });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(true);
    } finally {
      server.close();
    }
  });

  it("should fail when no response and default expect.responded=true", async () => {
    const server = await Bun.udpSocket({
      socket: {
        data() {
          // sink - 不回包以模拟无响应
        },
        drain() {
          // Bun UDP socket handler 必填项
        },
        error() {
          // Bun UDP socket handler 必填项
        },
      },
    });
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port });
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), 200);
      const result = await checker.execute(target, { signal: controller.signal });
      clearTimeout(timer);
      expect(result.matched).toBe(false);
      expect(result.failure).toMatchObject({ kind: "error", phase: "response" });
      expect(result.observation?.["durationMs"]).toBeGreaterThanOrEqual(0);
      expect(result.observation?.["error"]).toBeTruthy();
      expect(result.observation).toMatchObject({ responded: false });
    } finally {
      server.close();
    }
  });

  it("should match when expect.responded=false and no response", async () => {
    const server = await Bun.udpSocket({
      socket: {
        data() {
          // sink - 不回包以模拟无响应
        },
        drain() {
          // Bun UDP socket handler 必填项
        },
        error() {
          // Bun UDP socket handler 必填项
        },
      },
    });
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port }, { responded: false });
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), 200);
      const result = await checker.execute(target, { signal: controller.signal });
      clearTimeout(timer);
      expect(result.matched).toBe(true);
      expect(result.observation?.["durationMs"]).toBeGreaterThanOrEqual(0);
      expect(result.observation).toMatchObject({ error: null, responded: false });
    } finally {
      server.close();
    }
  });

  it("should fail when expect.responded=false but response received", async () => {
    const server = await createEchoServer();
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port }, { responded: false });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(false);
      expect(result.failure?.phase).toBe("responded");
    } finally {
      server.close();
    }
  });

  it("should validate response content with expect.response", async () => {
    const server = await createEchoServer();
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port }, { response: [{ contains: "PING" }] });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(true);
    } finally {
      server.close();
    }
  });

  it("should fail response content mismatch", async () => {
    const server = await createEchoServer();
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port }, { response: [{ contains: "PONG" }] });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(false);
      expect(result.failure?.phase).toBe("response");
    } finally {
      server.close();
    }
  });

  it("should validate responseSize", async () => {
    const server = await createEchoServer();
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port }, { responseSize: { gte: 1 } });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(true);
    } finally {
      server.close();
    }
  });

  it("should only use the first datagram when server sends multiple", async () => {
    const server = await Bun.udpSocket({
      socket: {
        data(sock, buf, port, addr) {
          sock.send(Buffer.from("FIRST"), port, addr);
          setTimeout(() => {
            try {
              sock.send(Buffer.from("SECOND"), port, addr);
            } catch {
              // checker 已关闭 connected socket，第二次发送可能失败
            }
          }, 30);
        },
        drain() {
          // Bun UDP socket handler 必填项
        },
        error() {
          // Bun UDP socket handler 必填项
        },
      },
    });
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ payload: "PING", port: server.port }, { response: [{ contains: "FIRST" }] });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(true);
      expect(result.observation).toMatchObject({ responded: true, responseSize: 5 });
    } finally {
      server.close();
    }
  });

  it("should fail when response exceeds maxResponseBytes", async () => {
    const server = await createEchoServer();
    try {
      const checker = new UdpChecker();
      const longPayload = "X".repeat(100);
      const target = makeTarget({
        maxResponseBytes: 10,
        payload: longPayload,
        port: server.port,
      });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(false);
      expect(result.failure?.phase).toBe("response");
      expect(result.failure?.kind).toBe("error");
    } finally {
      server.close();
    }
  });

  it("should fail when duration exceeds durationMs", async () => {
    const server = await Bun.udpSocket({
      socket: {
        data() {
          // sink - 不回包，通过 abort 触发 no-response 路径
        },
        drain() {
          // Bun UDP socket handler 必填项
        },
        error() {
          // Bun UDP socket handler 必填项
        },
      },
    });
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port }, { durationMs: { lte: 1 }, responded: false });
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), 200);
      const result = await checker.execute(target, { signal: controller.signal });
      clearTimeout(timer);
      expect(result.matched).toBe(false);
      expect(result.failure?.phase).toBe("duration");
    } finally {
      server.close();
    }
  });

  it("should match sourceHost assertion", async () => {
    const server = await createEchoServer();
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port }, { sourceHost: { equals: "127.0.0.1" } });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(true);
    } finally {
      server.close();
    }
  });

  it("should match sourcePort assertion", async () => {
    const server = await createEchoServer();
    try {
      const checker = new UdpChecker();
      const target = makeTarget({ port: server.port }, { sourcePort: { equals: server.port } });
      const { cleanup, signal } = makeSignal(5000);
      const result = await checker.execute(target, { signal });
      cleanup();
      expect(result.matched).toBe(true);
    } finally {
      server.close();
    }
  });
});

describe("UdpChecker resolve", () => {
  it("should fill defaults for minimal config", () => {
    const checker = new UdpChecker();
    const target = checker.resolve(
      { id: "test", type: "udp", udp: { host: "127.0.0.1", port: 9000 } },
      { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
    );
    expect(target.udp.payload).toBe("");
    expect(target.udp.encoding).toBe("text");
    expect(target.udp.responseEncoding).toBe("text");
    expect(target.udp.maxResponseBytes).toBe(4096);
    expect(target.rawExpect).toBeUndefined();
    expect(target.expect).toEqual({ responded: true });
  });

  it("should use defaults.udp for missing fields", () => {
    const checker = new UdpChecker();
    const target = checker.resolve(
      { id: "test", type: "udp", udp: { host: "127.0.0.1", port: 9000 } },
      {
        configDir: "/tmp",
        defaultIntervalMs: 30000,
        defaults: { udp: { encoding: "hex", maxResponseBytes: "8KB", responseEncoding: "hex" } },
        defaultTimeoutMs: 10000,
      },
    );
    expect(target.udp.encoding).toBe("hex");
    expect(target.udp.responseEncoding).toBe("hex");
    expect(target.udp.maxResponseBytes).toBe(8192);
  });

  it("should override defaults with target-level config", () => {
    const checker = new UdpChecker();
    const target = checker.resolve(
      { id: "test", type: "udp", udp: { encoding: "base64", host: "127.0.0.1", port: 9000 } },
      {
        configDir: "/tmp",
        defaultIntervalMs: 30000,
        defaults: { udp: { encoding: "hex" } },
        defaultTimeoutMs: 10000,
      },
    );
    expect(target.udp.encoding).toBe("base64");
  });
});

describe("UdpChecker serialize", () => {
  it("should produce udp host:port target summary", () => {
    const checker = new UdpChecker();
    const target = makeTarget({ host: "10.0.0.1", port: 9000 });
    const { config, target: display } = checker.serialize(target);
    expect(display).toBe("udp 10.0.0.1:9000");
    const parsed = JSON.parse(config) as Record<string, unknown>;
    expect(parsed["host"]).toBe("10.0.0.1");
    expect(parsed["port"]).toBe(9000);
  });
});
