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

import type {
  RawTcpExpectConfig,
  ResolvedTcpExpectConfig,
  ResolvedTcpTarget,
} from "../../../../../src/server/checker/runner/tcp/types";
import type { CheckerContext } from "../../../../../src/server/checker/runner/types";

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

const checker = new TcpChecker();

let server: Bun.TCPSocketListener;
let serverPort: number;
let bannerServer: Bun.TCPSocketListener;
let bannerServerPort: number;
let largeBannerServer: Bun.TCPSocketListener;
let largeBannerServerPort: number;

function makeCtx(timeoutMs = 5000): CheckerContext {
  const controller = new AbortController();
  setTimeout(() => controller.abort(), timeoutMs);
  return { signal: controller.signal };
}

function makeTarget(
  tcp: Partial<ResolvedTcpTarget["tcp"]>,
  overrides?: { expect?: RawTcpExpectConfig },
): ResolvedTcpTarget {
  const raw = overrides?.expect;
  const resolvedExpect: ResolvedTcpExpectConfig | undefined = raw
    ? {
        banner: resolveContentExpectations(raw.banner),
        connected: raw.connected ?? true,
        durationMs: resolveValueExpectation(raw.durationMs),
      }
    : undefined;

  return {
    description: null,
    expect: resolvedExpect,
    group: "default",
    id: "test-tcp",
    intervalMs: 60000,
    name: "test-tcp",
    rawExpect: raw,
    tcp: {
      bannerReadTimeout: 2000,
      host: "127.0.0.1",
      maxBannerBytes: 4096,
      port: serverPort,
      readBanner: false,
      ...tcp,
    },
    timeoutMs: 5000,
    type: "tcp",
  };
}

beforeAll(() => {
  server = Bun.listen({
    hostname: "127.0.0.1",
    port: 0,
    socket: {
      data() {
        // Bun.listen 必填 handler，echo server 不处理数据
      },
      end(socket) {
        try {
          socket.close();
        } catch {
          // best-effort 关闭
        }
      },
      error() {
        // Bun.listen 必填 handler，测试 server 忽略错误
      },
      open() {
        // Bun.listen 必填 handler，open 时无需操作
      },
    },
  });
  serverPort = server.port;

  bannerServer = Bun.listen({
    hostname: "127.0.0.1",
    port: 0,
    socket: {
      data() {
        // Bun.listen 必填 handler
      },
      end(socket) {
        try {
          socket.close();
        } catch {
          // best-effort 关闭
        }
      },
      error() {
        // Bun.listen 必填 handler
      },
      open(socket) {
        socket.write("220 smtp.example.com ESMTP\r\n");
      },
    },
  });
  bannerServerPort = bannerServer.port;

  largeBannerServer = Bun.listen({
    hostname: "127.0.0.1",
    port: 0,
    socket: {
      data() {
        // Bun.listen 必填 handler
      },
      end(socket) {
        try {
          socket.close();
        } catch {
          // best-effort 关闭
        }
      },
      error() {
        // Bun.listen 必填 handler
      },
      open(socket) {
        socket.write("X".repeat(8192));
      },
    },
  });
  largeBannerServerPort = largeBannerServer.port;
});

afterAll(() => {
  server.stop();
  bannerServer.stop();
  largeBannerServer.stop();
});

describe("TcpChecker execute", () => {
  test("TCP 连接成功", async () => {
    const result = await checker.execute(makeTarget({}), makeCtx());
    expect(result.matched).toBe(true);
    expect(result.failure).toBeNull();
    expect(result.durationMs).toBeGreaterThanOrEqual(0);
    expect(result.observation).toMatchObject({ connected: true });
  });

  test("TCP 连接失败", async () => {
    const result = await checker.execute(makeTarget({ host: "127.0.0.1", port: 1 }), makeCtx());
    expect(result.matched).toBe(false);
    expect(result.failure!.kind).toBe("error");
    expect(result.failure!.phase).toBe("connect");
    expect(result.failure!.message).toBeTruthy();
  });

  test("期望端口不可达且连接失败", async () => {
    const result = await checker.execute(
      makeTarget({ host: "127.0.0.1", port: 1 }, { expect: { connected: false } }),
      makeCtx(),
    );
    expect(result.matched).toBe(true);
    expect(result.failure).toBeNull();
    expect(result.observation).toMatchObject({ connected: false });
  });

  test("期望端口不可达但连接成功", async () => {
    const result = await checker.execute(makeTarget({}, { expect: { connected: false } }), makeCtx());
    expect(result.matched).toBe(false);
    expect(result.failure!.kind).toBe("mismatch");
    expect(result.failure!.phase).toBe("connected");
  });

  test("durationMs 超时返回失败", async () => {
    const result = await checker.execute(makeTarget({}, { expect: { durationMs: { lt: 0 } } }), makeCtx());
    expect(result.matched).toBe(false);
    expect(result.failure!.phase).toBe("duration");
  });

  test("读取服务端 banner 成功", async () => {
    const result = await checker.execute(
      makeTarget({
        host: "127.0.0.1",
        port: bannerServerPort,
        readBanner: true,
      }),
      makeCtx(),
    );
    expect(result.matched).toBe(true);
    const obs = result.observation!;
    expect(obs).toMatchObject({
      connected: true,
    });
    expect(obs["banner"]).toContain("220 smtp.example.com ESMTP");
  });

  test("banner operator 校验通过", async () => {
    const result = await checker.execute(
      makeTarget(
        { host: "127.0.0.1", port: bannerServerPort, readBanner: true },
        { expect: { banner: [{ contains: "ESMTP" }] } },
      ),
      makeCtx(),
    );
    expect(result.matched).toBe(true);
  });

  test("banner operator 校验失败", async () => {
    const result = await checker.execute(
      makeTarget(
        { host: "127.0.0.1", port: bannerServerPort, readBanner: true },
        { expect: { banner: [{ contains: "POSTFIX" }] } },
      ),
      makeCtx(),
    );
    expect(result.matched).toBe(false);
    expect(result.failure!.kind).toBe("mismatch");
    expect(result.failure!.phase).toBe("banner");
    expect(result.failure!.path).toBe("banner[0]");
  });

  test("默认不读取 banner", async () => {
    const result = await checker.execute(
      makeTarget({ host: "127.0.0.1", port: bannerServerPort, readBanner: false }),
      makeCtx(),
    );
    expect(result.matched).toBe(true);
    expect(result.observation?.["banner"]).toBeFalsy();
  });

  test("banner 超时空字符串继续执行", async () => {
    const result = await checker.execute(
      makeTarget({
        bannerReadTimeout: 100,
        host: "127.0.0.1",
        port: serverPort,
        readBanner: true,
      }),
      makeCtx(),
    );
    expect(result.matched).toBe(true);
  });

  test("banner 读取超过最大字节数", async () => {
    const result = await checker.execute(
      makeTarget({
        bannerReadTimeout: 2000,
        host: "127.0.0.1",
        maxBannerBytes: 1024,
        port: largeBannerServerPort,
        readBanner: true,
      }),
      makeCtx(),
    );
    expect(result.matched).toBe(false);
    expect(result.failure!.kind).toBe("error");
    expect(result.failure!.phase).toBe("banner");
    expect(result.failure!.message).toContain("字节限制");
  });

  test("TCP 执行超时（预 abort）", async () => {
    const controller = new AbortController();
    controller.abort();
    const result = await checker.execute(makeTarget({ host: "127.0.0.1", port: serverPort }), {
      signal: controller.signal,
    });
    expect(result.matched).toBe(false);
    expect(result.failure!.phase).toBe("connect");
  });

  test("banner 读取过程中 abort", async () => {
    const controller = new AbortController();
    setTimeout(() => controller.abort(), 10);
    const result = await checker.execute(
      makeTarget({
        bannerReadTimeout: 5000,
        host: "127.0.0.1",
        port: serverPort,
        readBanner: true,
      }),
      { signal: controller.signal },
    );
    expect(result.matched).toBe(false);
    expect(result.failure!.kind).toBe("error");
    expect(["connect", "banner"]).toContain(result.failure!.phase);
  });

  test("serialize 返回 host:port 和 config JSON", () => {
    const target = makeTarget({ host: "10.0.0.1", port: 8080 });
    const s = checker.serialize(target);
    expect(s.target).toBe("10.0.0.1:8080");
    const config = JSON.parse(s.config) as Record<string, unknown>;
    expect(config["host"]).toBe("10.0.0.1");
    expect(config["port"]).toBe(8080);
    expect(config["readBanner"]).toBe(false);
  });
});

describe("TcpChecker resolve", () => {
  test("最简 tcp 配置解析默认值", () => {
    const target = checker.resolve(
      { id: "t", tcp: { host: "127.0.0.1", port: 6379 }, type: "tcp" },
      { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
    );
    expect(target.tcp.host).toBe("127.0.0.1");
    expect(target.tcp.port).toBe(6379);
    expect(target.tcp.readBanner).toBe(false);
    expect(target.tcp.bannerReadTimeout).toBe(2000);
    expect(target.tcp.maxBannerBytes).toBe(4096);
    expect(target.group).toBe("default");
    expect(target.name).toBeNull();
    expect(target.intervalMs).toBe(30000);
    expect(target.timeoutMs).toBe(10000);
    expect(target.rawExpect).toBeUndefined();
    expect(target.expect).toEqual({ connected: true });
  });

  test("bannerReadTimeout 和 maxBannerBytes 支持 per-target 覆盖", () => {
    const target = checker.resolve(
      {
        id: "t",
        tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", maxBannerBytes: "1KB", port: 80, readBanner: true },
        type: "tcp",
      },
      { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
    );
    expect(target.tcp.bannerReadTimeout).toBe(5000);
    expect(target.tcp.maxBannerBytes).toBe(1024);
    expect(target.tcp.readBanner).toBe(true);
  });

  test("defaults.tcp 合并到 target", () => {
    const target = checker.resolve(
      { id: "t", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" },
      {
        configDir: "/tmp",
        defaultIntervalMs: 30000,
        defaults: { tcp: { bannerReadTimeout: 1000, maxBannerBytes: "8KB" } },
        defaultTimeoutMs: 10000,
      },
    );
    expect(target.tcp.bannerReadTimeout).toBe(1000);
    expect(target.tcp.maxBannerBytes).toBe(8192);
  });

  test("per-target 覆盖 defaults.tcp", () => {
    const target = checker.resolve(
      { id: "t", tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", port: 80 }, type: "tcp" },
      {
        configDir: "/tmp",
        defaultIntervalMs: 30000,
        defaults: { tcp: { bannerReadTimeout: 1000 } },
        defaultTimeoutMs: 10000,
      },
    );
    expect(target.tcp.bannerReadTimeout).toBe(5000);
  });

  test("maxBannerBytes 整数默认值解析", () => {
    const target = checker.resolve(
      { id: "t", tcp: { host: "127.0.0.1", maxBannerBytes: 2048, port: 80 }, type: "tcp" },
      { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
    );
    expect(target.tcp.maxBannerBytes).toBe(2048);
  });

  test("expect 配置解析", () => {
    const target = checker.resolve(
      {
        expect: { banner: [{ contains: "ESMTP" }], connected: false, durationMs: { lte: 5000 } },
        id: "t",
        tcp: { host: "127.0.0.1", port: 80, readBanner: true },
        type: "tcp",
      },
      { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
    );
    expect(target.expect).toEqual({
      banner: [{ kind: "value", matcher: { contains: "ESMTP" } }],
      connected: false,
      durationMs: { lte: 5000 },
    });
    expect(target.rawExpect).toEqual({ banner: [{ contains: "ESMTP" }], connected: false, durationMs: { lte: 5000 } });
  });

  test("name 和 group 解析", () => {
    const target = checker.resolve(
      { group: "infra", id: "t", name: "redis", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" },
      { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
    );
    expect(target.name).toBe("redis");
    expect(target.group).toBe("infra");
  });
});
