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

import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/cmd/types";
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
import type { ResolvedPingTarget } from "../../../src/server/checker/runner/icmp/types";
import type { ResolvedTcpTarget } from "../../../src/server/checker/runner/tcp/types";

import { loadConfig, parseDuration } from "../../../src/server/checker/config-loader";
import { checkValueExpectation } from "../../../src/server/checker/expect/value";
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 { readRuntimeConfig } from "../../../src/server/config";

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

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

describe("parseDuration", () => {
  test("解析秒", () => {
    expect(parseDuration("30s")).toBe(30000);
    expect(parseDuration("1s")).toBe(1000);
  });

  test("解析分钟", () => {
    expect(parseDuration("5m")).toBe(300000);
    expect(parseDuration("1m")).toBe(60000);
  });

  test("解析毫秒", () => {
    expect(parseDuration("500ms")).toBe(500);
    expect(parseDuration("100ms")).toBe(100);
  });

  test("解析小数", () => {
    expect(parseDuration("1.5s")).toBe(1500);
  });

  test("解析小时", () => {
    expect(parseDuration("2h")).toBe(7200000);
    expect(parseDuration("1h")).toBe(3600000);
  });

  test("解析天", () => {
    expect(parseDuration("7d")).toBe(604800000);
    expect(parseDuration("1d")).toBe(86400000);
  });

  test("拒绝非正整数毫秒结果", () => {
    expect(() => parseDuration("0ms")).toThrow("正整数毫秒");
    expect(() => parseDuration("1.5ms")).toThrow("正整数毫秒");
  });

  test("无效格式抛出错误", () => {
    expect(() => parseDuration("30")).toThrow("无效的时长格式");
    expect(() => parseDuration("abc")).toThrow("无效的时长格式");
    expect(() => parseDuration("30x")).toThrow("无效的时长格式");
    expect(() => parseDuration("")).toThrow("无效的时长格式");
  });
});

describe("readRuntimeConfig", () => {
  test("返回配置文件路径", () => {
    expect(readRuntimeConfig(["./probes.yaml"])).toEqual({ configPath: "./probes.yaml" });
  });

  test("未提供参数抛出错误", () => {
    expect(() => readRuntimeConfig([])).toThrow("需要指定 YAML 配置文件路径");
  });
});

describe("loadConfig", () => {
  let tempDir: string;

  beforeAll(async () => {
    tempDir = join(tmpdir(), `gc-test-${Date.now()}`);
    await mkdir(tempDir, { recursive: true });
  });

  afterAll(async () => {
    await rm(tempDir, { force: true, recursive: true });
  });

  async function expectConfigError(fileName: string, content: string, message: string): Promise<void> {
    const configPath = join(tempDir, fileName);
    await writeFile(configPath, content);
    let error: unknown;
    try {
      await loadConfig(configPath);
    } catch (caught) {
      error = caught;
    }
    expect(error).toBeInstanceOf(Error);
    expect((error as Error).message).toContain(message);
  }

  async function expectConfigLoadError(configPath: string, message: string): Promise<void> {
    let error: unknown;
    try {
      await loadConfig(configPath);
    } catch (caught) {
      error = caught;
    }
    expect(error).toBeInstanceOf(Error);
    expect((error as Error).message).toContain(message);
  }

  test("解析最简 HTTP 配置", async () => {
    const configPath = join(tempDir, "minimal-http.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
`,
    );

    const config = await loadConfig(configPath);
    expect(config.host).toBe("127.0.0.1");
    expect(config.port).toBe(3000);
    expect(config.dataDir).toBe(join(tempDir, "data"));
    expect(config.maxConcurrentChecks).toBe(20);
    expect(config.targets).toHaveLength(1);
    const t = config.targets[0]! as ResolvedHttpTarget;
    expect(t.type).toBe("http");
    expect(t.id).toBe("test");
    expect(t.name).toBe("test");
    expect(t.http.url).toBe("http://example.com");
    expect(t.http.method).toBe("GET");
    expect(t.http.headers).toEqual({});
    expect(t.http.ignoreSSL).toBe(false);
    expect(t.http.maxBodyBytes).toBe(104857600);
    expect(t.http.maxRedirects).toBe(0);
    expect(t.intervalMs).toBe(30000);
    expect(t.timeoutMs).toBe(10000);
  });

  test("解析最简 cmd 配置", async () => {
    const subdir = join(tempDir, "subdir");
    await mkdir(subdir, { recursive: true });
    const configPath = join(subdir, "cmd.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "check-nginx"
    id: "check-nginx"
    type: cmd
    cmd:
      exec: "pgrep"
      args: ["nginx"]
`,
    );

    const config = await loadConfig(configPath);
    expect(config.targets).toHaveLength(1);
    const t = config.targets[0]! as ResolvedCommandTarget;
    expect(t.type).toBe("cmd");
    expect(t.id).toBe("check-nginx");
    expect(t.name).toBe("check-nginx");
    expect(t.cmd.exec).toBe("pgrep");
    expect(t.cmd.args).toEqual(["nginx"]);
    expect(t.cmd.cwd).toBe(subdir);
    expect(t.cmd.maxOutputBytes).toBe(104857600);
    expect(Object.keys(t.cmd.env).some((key) => key.toUpperCase() === "PATH")).toBe(true);
  });

  test("解析完整配置", async () => {
    const configPath = join(tempDir, "full.yaml");
    await writeFile(
      configPath,
      `server:
  listen:
    host: "0.0.0.0"
    port: 8080
  storage:
    dataDir: "./my-data"
probes:
  execution:
    maxConcurrentChecks: 5
defaults:
  interval: "15s"
  timeout: "5s"
  http:
    headers:
      Authorization: "Bearer token"
    maxBodyBytes: "50MB"
  cmd:
    cwd: "/tmp"
    maxOutputBytes: "10MB"
targets:
  - name: "http-target"
    id: "http-target"
    type: http
    interval: "1m"
    http:
      url: "http://example.com"
      method: "POST"
      ignoreSSL: true
      maxRedirects: 5
    expect:
      status: ["2xx", 301]
      body:
        - contains: "ok"
  - name: "cmd-target"
    id: "cmd-target"
    type: cmd
    cmd:
      exec: "ls"
      args: ["/tmp"]
    expect:
      exitCode: [0]
`,
    );

    const config = await loadConfig(configPath);
    expect(config.host).toBe("0.0.0.0");
    expect(config.port).toBe(8080);
    expect(config.dataDir).toBe(join(tempDir, "my-data"));
    expect(config.maxConcurrentChecks).toBe(5);
    expect(config.targets).toHaveLength(2);

    const http = config.targets[0]! as ResolvedHttpTarget;
    expect(http.type).toBe("http");
    expect(http.http.url).toBe("http://example.com");
    expect(http.http.method).toBe("POST");
    expect(http.http.headers).toEqual({ Authorization: "Bearer token" });
    expect(http.http.ignoreSSL).toBe(true);
    expect(http.http.maxBodyBytes).toBe(52428800);
    expect(http.http.maxRedirects).toBe(5);
    expect(http.expect?.status).toEqual(["2xx", 301]);
    expect(http.intervalMs).toBe(60000);
    expect(http.timeoutMs).toBe(5000);

    const cmd = config.targets[1]! as ResolvedCommandTarget;
    expect(cmd.type).toBe("cmd");
    expect(cmd.cmd.exec).toBe("ls");
    expect(cmd.cmd.args).toEqual(["/tmp"]);
    expect(cmd.cmd.maxOutputBytes).toBe(10485760);
  });

  test("name 缺省时保留为 null", async () => {
    const configPath = join(tempDir, "name-fallback.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    type: http
    http:
      url: "http://example.com"
`,
    );

    const config = await loadConfig(configPath);
    const target = config.targets[0]!;
    expect(target.id).toBe("api-health");
    expect(target.name).toBeNull();
  });

  test("name 显式 null 保留为 null", async () => {
    const configPath = join(tempDir, "name-explicit-null.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    name: null
    type: http
    http:
      url: "http://example.com"
`,
    );

    const config = await loadConfig(configPath);
    expect(config.targets[0]!.name).toBeNull();
  });

  test("name YAML 空值保留为 null", async () => {
    const configPath = join(tempDir, "name-yaml-null.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    name:
    type: http
    http:
      url: "http://example.com"
`,
    );

    const config = await loadConfig(configPath);
    expect(config.targets[0]!.name).toBeNull();
  });

  test("ValueMatcher primitive 简写在 resolve 后可运行期匹配", async () => {
    const configPath = join(tempDir, "matcher-shorthand.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    type: http
    http:
      url: "http://example.com"
    expect:
      durationMs: 123
`,
    );

    const config = await loadConfig(configPath);
    const target = config.targets[0]! as ResolvedHttpTarget;

    expect(target.expect?.durationMs).toEqual({ equals: 123 });
    expect(
      checkValueExpectation(123, target.expect?.durationMs, {
        path: "durationMs",
        phase: "duration",
      }).matched,
    ).toBe(true);
  });

  test("name 为空字符串抛出错误", async () => {
    const configPath = join(tempDir, "empty-name.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    name: ""
    type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "name 不能为空白");
  });

  test("name 仅包含空白字符抛出错误", async () => {
    const configPath = join(tempDir, "whitespace-name.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    name: "   "
    type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "name 不能为空白");
  });

  test("description 显式 null 保留为 null", async () => {
    const configPath = join(tempDir, "description-null.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    description: null
    type: http
    http:
      url: "http://example.com"
`,
    );
    const config = await loadConfig(configPath);
    expect(config.targets[0]!.description).toBeNull();
  });

  test("description YAML 空值保留为 null", async () => {
    const configPath = join(tempDir, "description-yaml-null.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    description:
    type: http
    http:
      url: "http://example.com"
`,
    );
    const config = await loadConfig(configPath);
    expect(config.targets[0]!.description).toBeNull();
  });

  test("name 支持变量替换且不要求唯一", async () => {
    const configPath = join(tempDir, "name-variable.yaml");
    await writeFile(
      configPath,
      `variables:
  env: "生产"
targets:
  - id: "api-a"
    name: "\${env} API"
    type: http
    http:
      url: "http://a.example.com"
  - id: "api-b"
    name: "\${env} API"
    type: http
    http:
      url: "http://b.example.com"
`,
    );

    const config = await loadConfig(configPath);
    expect(config.targets.map((target) => [target.id, target.name])).toEqual([
      ["api-a", "生产 API"],
      ["api-b", "生产 API"],
    ]);
  });

  test("包含 variables 的完整配置在 schema 校验前完成替换", async () => {
    const configPath = join(tempDir, "variables-full.yaml");
    await writeFile(
      configPath,
      `variables:
  env: "生产"
  base_url: "https://example.com"
  ignore_ssl: true
  max_redirects: 5
targets:
  - id: "api-health"
    name: "\${env} API 健康检查"
    type: http
    http:
      url: "\${base_url}/health"
      ignoreSSL: "\${ignore_ssl}"
      maxRedirects: "\${max_redirects}"
`,
    );

    const config = await loadConfig(configPath);
    const target = config.targets[0] as ResolvedHttpTarget;
    expect(target.id).toBe("api-health");
    expect(target.name).toBe("生产 API 健康检查");
    expect(target.http.url).toBe("https://example.com/health");
    expect(target.http.ignoreSSL).toBe(true);
    expect(target.http.maxRedirects).toBe(5);
  });

  test("变量替换后类型不匹配导致 schema 校验失败", async () => {
    const configPath = join(tempDir, "bad-var-type.yaml");
    await writeFile(
      configPath,
      `variables:
    max_redirects: "not-a-number"
targets:
  - id: "bad-var"
    type: http
    http:
      url: "http://example.com"
      maxRedirects: "\${max_redirects}"
`,
    );
    await expectConfigLoadError(configPath, "maxRedirects");
  });

  test("变量替换后通过 schema 校验", async () => {
    const configPath = join(tempDir, "good-var-type.yaml");
    const origEnv = process.env["DIAL_VAR_MAX_REDIRECTS"];
    process.env["DIAL_VAR_MAX_REDIRECTS"] = "3";
    try {
      await writeFile(
        configPath,
        `targets:
  - id: "good-var"
    type: http
    http:
      url: "http://example.com"
      maxRedirects: "\${DIAL_VAR_MAX_REDIRECTS}"
`,
      );
      const config = await loadConfig(configPath);
      const target = config.targets[0] as ResolvedHttpTarget;
      expect(target.http.maxRedirects).toBe(3);
    } finally {
      if (origEnv === undefined) {
        delete process.env["DIAL_VAR_MAX_REDIRECTS"];
      } else {
        process.env["DIAL_VAR_MAX_REDIRECTS"] = origEnv;
      }
    }
  });

  test("未定义变量且无默认值阻止启动", async () => {
    const configPath = join(tempDir, "unresolved-var.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "unresolved"
    type: http
    http:
      url: "\${MISSING_BASE_URL}/health"
`,
    );
    await expectConfigLoadError(configPath, "未定义的变量");
  });

  test("绝对 dataDir 保持不变", async () => {
    const dataDir = join(tempDir, "absolute-data");
    const configPath = join(tempDir, "absolute-data-dir.yaml");
    await writeFile(
      configPath,
      `server:
  storage:
    dataDir: ${JSON.stringify(dataDir)}
targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
`,
    );

    const config = await loadConfig(configPath);
    expect(config.dataDir).toBe(dataDir);
  });

  test("per-target 覆盖 defaults", async () => {
    const configPath = join(tempDir, "override.yaml");
    await writeFile(
      configPath,
      `defaults:
  interval: "30s"
  timeout: "10s"
  http:
    maxBodyBytes: "10MB"
targets:
  - name: "override-all"
    id: "override-all"
    type: http
    interval: "5m"
    timeout: "30s"
    http:
      url: "http://example.com"
      method: "POST"
      maxBodyBytes: "1MB"
`,
    );

    const config = await loadConfig(configPath);
    const t = config.targets[0]! as ResolvedHttpTarget;
    expect(t.http.method).toBe("POST");
    expect(t.intervalMs).toBe(300000);
    expect(t.timeoutMs).toBe(30000);
    expect(t.http.maxBodyBytes).toBe(1048576);
  });

  test("配置文件不存在抛出错误", async () => {
    await expectConfigLoadError("/nonexistent/file.yaml", "配置文件不存在");
  });

  test("target 缺少 id 抛出错误", async () => {
    const configPath = join(tempDir, "no-id.yaml");
    await writeFile(
      configPath,
      `targets:
  - type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "缺少 id 字段");
  });

  test("target 缺少 type 抛出错误", async () => {
    const configPath = join(tempDir, "no-type.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "缺少 type 字段");
  });

  test("HTTP target 缺少 url 抛出错误", async () => {
    const configPath = join(tempDir, "no-url.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http: {}
`,
    );
    await expectConfigLoadError(configPath, "缺少 http.url 字段");
  });

  test("HTTP target 缺少 http 分组抛出清晰错误", async () => {
    const configPath = join(tempDir, "no-http-group.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
`,
    );
    await expectConfigLoadError(configPath, "缺少 http.url 字段");
  });

  test("HTTP target ignoreSSL 非布尔值抛出错误", async () => {
    const configPath = join(tempDir, "bad-ignore-ssl.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
      ignoreSSL: "true"
`,
    );
    await expectConfigLoadError(configPath, "http.ignoreSSL 类型不合法");
  });

  test("HTTP target maxRedirects 非负整数校验", async () => {
    const configPath = join(tempDir, "bad-max-redirects.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
      maxRedirects: 1.5
`,
    );
    await expectConfigLoadError(configPath, "http.maxRedirects 类型不合法");
  });

  test("HTTP target status 模式非法抛出错误", async () => {
    const configPath = join(tempDir, "bad-status-pattern.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      status: ["abc"]
`,
    );
    await expectConfigLoadError(configPath, "status 模式");
  });

  test("cmd target 缺少 exec 抛出错误", async () => {
    const configPath = join(tempDir, "no-exec.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: cmd
    cmd: {}
`,
    );
    await expectConfigLoadError(configPath, "缺少 cmd.exec 字段");
  });

  test("非法 target type 抛出错误", async () => {
    const configPath = join(tempDir, "bad-type.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: dns
`,
    );
    await expectConfigLoadError(configPath, "不支持的 type");
  });

  test("target id 重复抛出错误", async () => {
    const configPath = join(tempDir, "dup-id.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "dup"
    id: "dup"
    type: http
    http:
      url: "http://a.com"
  - name: "dup"
    id: "dup"
    type: http
    http:
      url: "http://b.com"
`,
    );
    await expectConfigLoadError(configPath, "target id 重复");
  });

  test("target id 为空字符串抛出错误", async () => {
    const configPath = join(tempDir, "empty-id.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: ""
    type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "缺少 id 字段");
  });

  test("target id 命名不合法抛出错误", async () => {
    const configPath = join(tempDir, "bad-id.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "_invalid"
    type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "id 不符合命名规则");
  });

  test("target id 包含下划线和连字符通过", async () => {
    const configPath = join(tempDir, "id-underscore-dash.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "db_check-01"
    type: http
    http:
      url: "http://example.com"
`,
    );
    const config = await loadConfig(configPath);
    expect(config.targets[0]!.id).toBe("db_check-01");
  });

  test("targets 为空数组抛出错误", async () => {
    const configPath = join(tempDir, "empty-targets.yaml");
    await writeFile(configPath, `targets: []`);
    await expectConfigLoadError(configPath, "至少一个 target");
  });

  test("无效端口号抛出错误", async () => {
    const configPath = join(tempDir, "bad-port.yaml");
    await writeFile(
      configPath,
      `server:
  listen:
    port: 99999
targets:
  - name: "t"
    id: "t"
    type: http
    http:
      url: "http://a.com"
`,
    );
    await expectConfigLoadError(configPath, "server.listen.port 数值范围不合法");
  });

  test("非法 maxConcurrentChecks 抛出错误", async () => {
    const configPath = join(tempDir, "bad-concurrency.yaml");
    await writeFile(
      configPath,
      `probes:
  execution:
    maxConcurrentChecks: -1
targets:
  - name: "t"
    id: "t"
    type: http
    http:
      url: "http://a.com"
`,
    );
    await expectConfigLoadError(configPath, "probes.execution.maxConcurrentChecks 数值范围不合法");
  });

  test("非法 size 格式抛出错误", async () => {
    const configPath = join(tempDir, "bad-size.yaml");
    await writeFile(
      configPath,
      `defaults:
  http:
    maxBodyBytes: "100TB"
targets:
  - name: "t"
    id: "t"
    type: http
    http:
      url: "http://a.com"
`,
    );
    await expectConfigLoadError(configPath, "无效的 size 格式");
  });

  test("非法 interval 格式抛出错误", async () => {
    const configPath = join(tempDir, "bad-interval.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "t"
    id: "t"
    type: http
    interval: "30x"
    http:
      url: "http://a.com"
`,
    );
    await expectConfigLoadError(configPath, "无效的时长格式");
  });

  test("解析 expect 配置", async () => {
    const configPath = join(tempDir, "expect.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "with-expect"
    id: "with-expect"
    type: http
    http:
      url: "http://example.com"
    expect:
      status: [200, 201]
      body:
        - contains: "ok"
        - json:
            path: "$.status"
            equals: "ok"
      durationMs:
        lte: 3000
`,
    );

    const config = await loadConfig(configPath);
    const t = config.targets[0]!;
    if (t.type === "http") {
      expect(t.rawExpect).toEqual({
        body: [{ contains: "ok" }, { json: { equals: "ok", path: "$.status" } }],
        durationMs: { lte: 3000 },
        status: [200, 201],
      });
      expect(t.expect).toEqual({
        body: [
          { kind: "value", matcher: { contains: "ok" } },
          { kind: "json", matcher: { equals: "ok" }, path: "$.status" },
        ],
        durationMs: { lte: 3000 },
        status: [200, 201],
      });
    }
  });

  test("解析 cmd expect 配置", async () => {
    const configPath = join(tempDir, "cmd-expect.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "cmd-with-expect"
    id: "cmd-with-expect"
    type: cmd
    cmd:
      exec: "mycheck"
    expect:
      exitCode: [0, 2]
      stdout:
        - contains: "ok"
        - regex: "done"
      stderr:
        - empty: true
      durationMs:
        lte: 5000
`,
    );

    const config = await loadConfig(configPath);
    const t = config.targets[0]!;
    if (t.type === "cmd") {
      expect(t.rawExpect).toEqual({
        durationMs: { lte: 5000 },
        exitCode: [0, 2],
        stderr: [{ empty: true }],
        stdout: [{ contains: "ok" }, { regex: "done" }],
      });
      expect(t.expect).toEqual({
        durationMs: { lte: 5000 },
        exitCode: [0, 2],
        stderr: [{ kind: "value", matcher: { empty: true } }],
        stdout: [
          { kind: "value", matcher: { contains: "ok" } },
          { kind: "value", matcher: { regex: "done" } },
        ],
      });
    }
  });

  test("cmd cwd 相对配置文件目录", async () => {
    const subdir = join(tempDir, "cwd-test");
    await mkdir(subdir, { recursive: true });
    const configPath = join(subdir, "cwd.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "cwd-test"
    id: "cwd-test"
    type: cmd
    cmd:
      exec: "ls"
      cwd: "scripts"
`,
    );

    const config = await loadConfig(configPath);
    const t = config.targets[0] as ResolvedCommandTarget;
    expect(t.cmd.cwd).toBe(join(subdir, "scripts"));
  });

  test("cmd env 覆盖", async () => {
    const configPath = join(tempDir, "env.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "env-test"
    id: "env-test"
    type: cmd
    cmd:
      exec: "echo"
      env:
        LANG: "C"
        CUSTOM_VAR: "test"
`,
    );

    const config = await loadConfig(configPath);
    const t = config.targets[0] as ResolvedCommandTarget;
    expect(t.cmd.env["LANG"]).toBe("C");
    expect(t.cmd.env["CUSTOM_VAR"]).toBe("test");
    expect(Object.keys(t.cmd.env).some((key) => key.toUpperCase() === "PATH")).toBe(true);
  });

  test("解析 group 字段", async () => {
    const configPath = join(tempDir, "group.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "grouped"
    id: "grouped"
    type: http
    group: "搜索引擎"
    http:
      url: "http://example.com"
`,
    );

    const config = await loadConfig(configPath);
    expect(config.targets[0]!.group).toBe("搜索引擎");
  });

  test("group 字段默认为 default", async () => {
    const configPath = join(tempDir, "no-group.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "no-group"
    id: "no-group"
    type: http
    http:
      url: "http://example.com"
`,
    );

    const config = await loadConfig(configPath);
    expect(config.targets[0]!.group).toBe("default");
  });

  test("非法 group 类型抛出错误", async () => {
    const configPath = join(tempDir, "bad-group.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    group: 123
    http:
      url: "http://example.com"
`,
    );

    await expectConfigLoadError(configPath, "group 必须为字符串");
  });

  test("HTTP headers 非字符串值抛出错误", async () => {
    const configPath = join(tempDir, "bad-headers-val.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
      headers:
        X-Custom: 123
`,
    );
    await expectConfigLoadError(configPath, "http.headers");
  });

  test("HTTP body 非字符串抛出错误", async () => {
    const configPath = join(tempDir, "bad-body.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
      body: 123
`,
    );
    await expectConfigLoadError(configPath, "http.body 类型不合法");
  });

  test("maxBodyBytes 负数抛出错误", async () => {
    const configPath = join(tempDir, "neg-bodybytes.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
      maxBodyBytes: -1
`,
    );
    await expectConfigLoadError(configPath, "非负安全整数");
  });

  test("maxBodyBytes 非整数抛出错误", async () => {
    const configPath = join(tempDir, "float-bodybytes.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
      maxBodyBytes: 1.5
`,
    );
    await expectConfigLoadError(configPath, "非负安全整数");
  });

  test("expect.status 数字不在 100-599 范围抛出错误", async () => {
    const configPath = join(tempDir, "bad-status-num.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      status: [999]
`,
    );
    await expectConfigLoadError(configPath, "100-599");
  });

  test("expect.status 范围 6xx 抛出错误", async () => {
    const configPath = join(tempDir, "bad-status-6xx.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      status: ["6xx"]
`,
    );
    await expectConfigLoadError(configPath, "5xx");
  });

  test("expect.durationMs 对象简写抛出错误", async () => {
    const configPath = join(tempDir, "bad-duration-object.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      durationMs:
        foo: "bar"
`,
    );
    await expectConfigLoadError(configPath, "expect.durationMs.foo 是未知 matcher");
  });

  test("expect.body 非数组抛出错误", async () => {
    const configPath = join(tempDir, "bad-expect-body.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body: "not-array"
`,
    );
    await expectConfigLoadError(configPath, "expect.body 必须为数组");
  });

  test("body rule 缺少支持字段抛出错误", async () => {
    const configPath = join(tempDir, "bad-body-rule-nofield.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body:
        - foo: "bar"
`,
    );
    await expectConfigLoadError(configPath, "foo 是未知字段");
  });

  test("body rule 使用 match 字段（非支持）抛出错误", async () => {
    const configPath = join(tempDir, "bad-body-rule-match.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body:
        - match: "ok"
`,
    );
    await expectConfigLoadError(configPath, "match 是未知字段");
  });

  test("body rule 直接 matcher 混入 extractor 抛出错误", async () => {
    const configPath = join(tempDir, "bad-body-rule-multi.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body:
        - contains: "ok"
          json:
            path: "$.status"
`,
    );
    await expectConfigLoadError(configPath, "直接 matcher 不能与 extractor 混用");
  });

  test("body regex 非法正则抛出错误", async () => {
    const configPath = join(tempDir, "bad-body-regex.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body:
        - regex: "[invalid"
`,
    );
    await expectConfigLoadError(configPath, "regex 正则不合法");
  });

  test("body json path 不以 $. 开头抛出错误", async () => {
    const configPath = join(tempDir, "bad-json-path.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body:
        - json:
            path: "status"
            equals: "ok"
`,
    );
    await expectConfigLoadError(configPath, "json.path");
  });

  test("body css selector 为空抛出错误", async () => {
    const configPath = join(tempDir, "bad-css-sel.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body:
        - css:
            selector: ""
`,
    );
    await expectConfigLoadError(configPath, "css.selector 必须为非空字符串");
  });

  test("旧 match matcher 抛出错误", async () => {
    const configPath = join(tempDir, "bad-op-match.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      headers:
        X-Test:
          match: "[invalid"
`,
    );
    await expectConfigLoadError(configPath, "match 是未知 matcher");
  });

  test("operator gte 非数字抛出错误", async () => {
    const configPath = join(tempDir, "bad-op-gte.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body:
        - json:
            path: "$.count"
            gte: "abc"
`,
    );
    await expectConfigLoadError(configPath, "gte 必须为有限数字");
  });

  test("operator exists 非布尔值抛出错误", async () => {
    const configPath = join(tempDir, "bad-op-exists.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body:
        - json:
            path: "$.status"
            exists: "yes"
`,
    );
    await expectConfigLoadError(configPath, "exists 必须为布尔值");
  });

  test("未知字段导致启动失败", async () => {
    const configPath = join(tempDir, "unknown-fields.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
      unknownHttpField: "value"
    expect:
      status: [200]
      unknownExpectField: "value"
      body:
        - contains: "ok"
          note: "ignored"
`,
    );
    await expectConfigLoadError(configPath, "unknownHttpField 是未知字段");
  });

  test("xpath path 非空字符串校验", async () => {
    const configPath = join(tempDir, "bad-xpath-path.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      body:
        - xpath:
            path: ""
`,
    );
    await expectConfigLoadError(configPath, "xpath.path 必须为非空字符串");
  });

  test("expect headers 非对象抛出错误", async () => {
    const configPath = join(tempDir, "bad-expect-headers.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
    expect:
      headers: "invalid"
`,
    );
    await expectConfigLoadError(configPath, "expect.headers 类型不合法");
  });

  test("HTTP method 小写输入失败", async () => {
    await expectConfigError(
      "lowercase-method.yaml",
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
      method: get
`,
      "http.method 不在允许范围内",
    );
  });

  test("defaults.http.method 触发未知字段错误", async () => {
    await expectConfigError(
      "unknown-default-method.yaml",
      `defaults:
  http:
    method: POST
targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
`,
      "defaults.http.method 是未知字段",
    );
  });

  test("HTTP method 大写输入通过", async () => {
    const configPath = join(tempDir, "uppercase-method.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
      method: POST
`,
    );
    const config = await loadConfig(configPath);
    const target = config.targets[0] as ResolvedHttpTarget;
    expect(target.type).toBe("http");
    expect(target.http.method).toBe("POST");
  });

  test("动态 headers 和 env 允许任意键名", async () => {
    const configPath = join(tempDir, "dynamic-maps.yaml");
    await writeFile(
      configPath,
      `defaults:
  http:
    headers:
      X-Default-Header: "default"
targets:
  - name: "http-test"
    id: "http-test"
    type: http
    http:
      url: "http://example.com"
      headers:
        X-Custom-Header: "custom"
    expect:
      headers:
        X-Response-Header:
          contains: "ok"
  - name: "cmd-test"
    id: "cmd-test"
    type: cmd
    cmd:
      exec: "true"
      env:
        CUSTOM_ENV_NAME: "custom"
`,
    );
    const config = await loadConfig(configPath);
    const http = config.targets[0] as ResolvedHttpTarget;
    const cmdTarget = config.targets[1] as ResolvedCommandTarget;
    expect(http.type).toBe("http");
    expect(cmdTarget.type).toBe("cmd");
    expect(http.http.headers["X-Default-Header"]).toBe("default");
    expect(http.http.headers["X-Custom-Header"]).toBe("custom");
    expect(cmdTarget.cmd.env["CUSTOM_ENV_NAME"]).toBe("custom");
  });

  test("cmd args 类型非法", async () => {
    await expectConfigError(
      "bad-cmd-args.yaml",
      `targets:
  - name: "cmd"
    id: "cmd"
    type: cmd
    cmd:
      exec: "echo"
      args: "hello"
`,
      "cmd.args 类型不合法",
    );
  });

  test("cmd cwd 类型非法", async () => {
    await expectConfigError(
      "bad-cmd-cwd.yaml",
      `targets:
  - name: "cmd"
    id: "cmd"
    type: cmd
    cmd:
      exec: "echo"
      cwd: 123
`,
      "cmd.cwd 类型不合法",
    );
  });

  test("cmd env 值类型非法", async () => {
    await expectConfigError(
      "bad-cmd-env.yaml",
      `targets:
  - name: "cmd"
    id: "cmd"
    type: cmd
    cmd:
      exec: "echo"
      env:
        COUNT: 123
`,
      "cmd.env.COUNT 类型不合法",
    );
  });

  test("cmd maxOutputBytes 非法", async () => {
    await expectConfigError(
      "bad-cmd-max-output.yaml",
      `targets:
  - name: "cmd"
    id: "cmd"
    type: cmd
    cmd:
      exec: "echo"
      maxOutputBytes: "1TB"
`,
      "maxOutputBytes 无效的 size 格式",
    );
  });

  test("cmd expect exitCode 类型非法", async () => {
    await expectConfigError(
      "bad-cmd-exit-code.yaml",
      `targets:
  - name: "cmd"
    id: "cmd"
    type: cmd
    cmd:
      exec: "echo"
    expect:
      exitCode: [1.5]
`,
      "expect.exitCode[0] 类型不合法",
    );
  });

  test("cmd stdout 空 text rule 非法", async () => {
    await expectConfigError(
      "bad-cmd-stdout-empty.yaml",
      `targets:
  - name: "cmd"
    id: "cmd"
    type: cmd
    cmd:
      exec: "echo"
    expect:
      stdout:
        - {}
`,
      "stdout[0] 必须包含至少一个合法 matcher",
    );
  });

  test("cmd stderr 未知 operator 非法", async () => {
    await expectConfigError(
      "bad-cmd-stderr-operator.yaml",
      `targets:
  - name: "cmd"
    id: "cmd"
    type: cmd
    cmd:
      exec: "echo"
    expect:
      stderr:
        - foo: "bar"
`,
      "expect.stderr[0].foo 是未知字段",
    );
  });

  test("cmd stdout 旧 match 字段非法", async () => {
    await expectConfigError(
      "bad-cmd-stdout-regex.yaml",
      `targets:
  - name: "cmd"
    id: "cmd"
    type: cmd
    cmd:
      exec: "echo"
    expect:
      stdout:
        - match: "[invalid"
`,
      "stdout[0].match 是未知字段",
    );
  });

  test("cmd expect 未知字段失败", async () => {
    await expectConfigError(
      "bad-cmd-expect-unknown.yaml",
      `targets:
  - name: "cmd"
    id: "cmd"
    type: cmd
    cmd:
      exec: "echo"
    expect:
      status: [200]
`,
      "expect.status 是未知字段",
    );
  });

  test("retention 默认值为 7d", async () => {
    const configPath = join(tempDir, "retention-default.yaml");
    await writeFile(
      configPath,
      `targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
`,
    );
    const config = await loadConfig(configPath);
    expect(config.retentionMs).toBe(604800000);
  });

  test("retention 自定义值", async () => {
    const configPath = join(tempDir, "retention-custom.yaml");
    await writeFile(
      configPath,
      `server:
  storage:
    retention: "24h"
targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
`,
    );
    const config = await loadConfig(configPath);
    expect(config.retentionMs).toBe(86400000);
  });

  test("retention 非法格式抛出错误", async () => {
    await expectConfigError(
      "bad-retention.yaml",
      `server:
  storage:
    retention: "7x"
targets:
  - name: "test"
    id: "test"
    type: http
    http:
      url: "http://example.com"
`,
      "无效的时长格式",
    );
  });

  test("解析 description 字段", async () => {
    const configPath = join(tempDir, "description.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    description: "检查生产 API 健康状态"
    type: http
    http:
      url: "http://example.com"
`,
    );
    const config = await loadConfig(configPath);
    expect(config.targets[0]!.description).toBe("检查生产 API 健康状态");
  });

  test("description 使用变量替换", async () => {
    const configPath = join(tempDir, "description-var.yaml");
    await writeFile(
      configPath,
      `variables:
  env: "生产"
targets:
  - id: "api-health"
    description: "\${env} 环境健康检查"
    type: http
    http:
      url: "http://example.com"
`,
    );
    const config = await loadConfig(configPath);
    expect(config.targets[0]!.description).toBe("生产 环境健康检查");
  });

  test("description 缺省为 null", async () => {
    const configPath = join(tempDir, "no-description.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    type: http
    http:
      url: "http://example.com"
`,
    );
    const config = await loadConfig(configPath);
    expect(config.targets[0]!.description).toBeNull();
  });

  test("description 为空字符串通过", async () => {
    const configPath = join(tempDir, "empty-description.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    description: ""
    type: http
    http:
      url: "http://example.com"
`,
    );
    const config = await loadConfig(configPath);
    expect(config.targets[0]!.description).toBe("");
  });

  test("description 非字符串抛出错误", async () => {
    const configPath = join(tempDir, "bad-description-type.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    description: 123
    type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "description");
  });

  test("description 超过 500 字符抛出错误", async () => {
    const configPath = join(tempDir, "long-description.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "api-health"
    description: "${"a".repeat(501)}"
    type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "description");
  });

  test("变量替换后 description 超长抛出错误", async () => {
    const configPath = join(tempDir, "var-long-description.yaml");
    await writeFile(
      configPath,
      `variables:
  prefix: "${"x".repeat(490)}"
targets:
  - id: "api-health"
    description: "\${prefix}${"a".repeat(15)}"
    type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "description");
  });

  test("id 超过 30 字符抛出错误", async () => {
    const configPath = join(tempDir, "long-id.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "${"a".repeat(31)}"
    type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "id");
  });

  test("name 超过 30 字符抛出错误", async () => {
    const configPath = join(tempDir, "long-name.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "test"
    name: "${"a".repeat(31)}"
    type: http
    http:
      url: "http://example.com"
`,
    );
    await expectConfigLoadError(configPath, "name");
  });

  test("解析最简 tcp 配置", async () => {
    const configPath = join(tempDir, "minimal-tcp.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "redis-port"
    type: tcp
    tcp:
      host: "127.0.0.1"
      port: 6379
`,
    );

    const config = await loadConfig(configPath);
    expect(config.targets).toHaveLength(1);
    const t = config.targets[0]! as ResolvedTcpTarget;
    expect(t.type).toBe("tcp");
    expect(t.id).toBe("redis-port");
    expect(t.name).toBeNull();
    expect(t.tcp.host).toBe("127.0.0.1");
    expect(t.tcp.port).toBe(6379);
    expect(t.tcp.readBanner).toBe(false);
    expect(t.tcp.bannerReadTimeout).toBe(2000);
    expect(t.tcp.maxBannerBytes).toBe(4096);
    expect(t.group).toBe("default");
    expect(t.intervalMs).toBe(30000);
    expect(t.timeoutMs).toBe(10000);
  });

  test("tcp 缺少 host 抛出错误", async () => {
    await expectConfigError(
      "tcp-no-host.yaml",
      `targets:
  - id: "t"
    type: tcp
    tcp:
      port: 80
`,
      "tcp.host",
    );
  });

  test("tcp 缺少 port 抛出错误", async () => {
    await expectConfigError(
      "tcp-no-port.yaml",
      `targets:
  - id: "t"
    type: tcp
    tcp:
      host: "127.0.0.1"
`,
      "tcp.port",
    );
  });

  test("tcp 非法端口范围抛出错误", async () => {
    await expectConfigError(
      "tcp-bad-port.yaml",
      `targets:
  - id: "t"
    type: tcp
    tcp:
      host: "127.0.0.1"
      port: 99999
`,
      "tcp.port",
    );
  });

  test("tcp 未知分组字段抛出错误", async () => {
    await expectConfigError(
      "tcp-unknown-field.yaml",
      `targets:
  - id: "t"
    type: tcp
    tcp:
      host: "127.0.0.1"
      port: 80
      tls: true
`,
      "是未知字段",
    );
  });

  test("tcp readBanner 开启并配置 expect.banner", async () => {
    const configPath = join(tempDir, "tcp-banner.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "smtp-check"
    type: tcp
    tcp:
      host: "127.0.0.1"
      port: 25
      readBanner: true
    expect:
      banner:
        - contains: "ESMTP"
`,
    );

    const config = await loadConfig(configPath);
    const t = config.targets[0]! as ResolvedTcpTarget;
    expect(t.tcp.readBanner).toBe(true);
    expect(t.expect?.banner).toEqual([{ kind: "value", matcher: { contains: "ESMTP" } }]);
  });

  test("tcp expect.banner 未开启 readBanner 抛出错误", async () => {
    await expectConfigError(
      "tcp-banner-no-read.yaml",
      `targets:
  - id: "t"
    type: tcp
    tcp:
      host: "127.0.0.1"
      port: 25
    expect:
      banner:
        contains: "ESMTP"
`,
      "banner 断言需要启用 tcp.readBanner",
    );
  });

  test("tcp defaults 覆盖 banner 参数", async () => {
    const configPath = join(tempDir, "tcp-defaults.yaml");
    await writeFile(
      configPath,
      `defaults:
  tcp:
    bannerReadTimeout: 1000
    maxBannerBytes: "8KB"
targets:
  - id: "t1"
    type: tcp
    tcp:
      host: "127.0.0.1"
      port: 80
  - id: "t2"
    type: tcp
    tcp:
      host: "127.0.0.1"
      port: 81
      bannerReadTimeout: 3000
`,
    );

    const config = await loadConfig(configPath);
    const t1 = config.targets[0]! as ResolvedTcpTarget;
    expect(t1.tcp.bannerReadTimeout).toBe(1000);
    expect(t1.tcp.maxBannerBytes).toBe(8192);

    const t2 = config.targets[1]! as ResolvedTcpTarget;
    expect(t2.tcp.bannerReadTimeout).toBe(3000);
    expect(t2.tcp.maxBannerBytes).toBe(8192);
  });

  test("tcp expect 未知字段抛出错误", async () => {
    await expectConfigError(
      "tcp-unknown-expect.yaml",
      `targets:
  - id: "t"
    type: tcp
    tcp:
      host: "127.0.0.1"
      port: 80
    expect:
      status: [200]
`,
      "是未知字段",
    );
  });

  test("tcp expect connected 和 durationMs", async () => {
    const configPath = join(tempDir, "tcp-expect-connected.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "t"
    type: tcp
    tcp:
      host: "127.0.0.1"
      port: 80
    expect:
      connected: false
      durationMs:
        lte: 5000
`,
    );

    const config = await loadConfig(configPath);
    const t = config.targets[0]! as ResolvedTcpTarget;
    expect(t.expect?.connected).toBe(false);
    expect(t.expect?.durationMs).toEqual({ lte: 5000 });
  });

  test("解析最简 icmp 配置", async () => {
    const configPath = join(tempDir, "minimal-icmp.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "gateway"
    type: icmp
    icmp:
      host: "10.0.0.1"
`,
    );

    const config = await loadConfig(configPath);
    expect(config.targets).toHaveLength(1);
    const t = config.targets[0]! as ResolvedPingTarget;
    expect(t.type).toBe("icmp");
    expect(t.icmp).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 });
    expect(t.group).toBe("default");
    expect(t.intervalMs).toBe(30000);
    expect(t.timeoutMs).toBe(10000);
  });

  test("解析 icmp expect 配置", async () => {
    const configPath = join(tempDir, "icmp-expect.yaml");
    await writeFile(
      configPath,
      `targets:
  - id: "gateway"
    type: icmp
    icmp:
      host: "10.0.0.1"
      count: 5
      packetSize: 1472
    expect:
      alive: true
      packetLossPercent:
        lte: 10
      avgLatencyMs:
        lte: 200
      maxLatencyMs:
        lte: 500
      durationMs:
        lte: 5000
`,
    );

    const config = await loadConfig(configPath);
    const t = config.targets[0]! as ResolvedPingTarget;
    expect(t.icmp).toEqual({ count: 5, host: "10.0.0.1", packetSize: 1472 });
    expect(t.expect).toEqual({
      alive: true,
      avgLatencyMs: { lte: 200 },
      durationMs: { lte: 5000 },
      maxLatencyMs: { lte: 500 },
      packetLossPercent: { lte: 10 },
    });
  });

  test("icmp 缺少 host 抛出错误", async () => {
    await expectConfigError(
      "icmp-no-host.yaml",
      `targets:
  - id: "gateway"
    type: icmp
    icmp: {}
`,
      "icmp.host",
    );
  });

  test("icmp count 非法抛出错误", async () => {
    await expectConfigError(
      "icmp-bad-count.yaml",
      `targets:
  - id: "gateway"
    type: icmp
    icmp:
      host: "10.0.0.1"
      count: 0
`,
      "icmp.count",
    );
  });

  test("icmp expect 未知字段抛出错误", async () => {
    await expectConfigError(
      "icmp-unknown-expect.yaml",
      `targets:
  - id: "gateway"
    type: icmp
    icmp:
      host: "10.0.0.1"
    expect:
      status: [200]
`,
      "expect.status 是未知字段",
    );
  });

  describe("logging 配置", () => {
    test("logging 全部缺省时使用默认值", async () => {
      const configPath = join(tempDir, "logging-default.yaml");
      await writeFile(
        configPath,
        `targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
      );
      const config = await loadConfig(configPath);
      expect(config.logging.consoleLevel).toBe("info");
      expect(config.logging.fileLevel).toBe("info");
      expect(config.logging.filePath).toBe(join(config.dataDir, "logs/dial.log"));
      expect(config.logging.rotationFrequency).toBe("daily");
      expect(config.logging.rotationMaxFiles).toBe(14);
      expect(config.logging.rotationSizeRaw).toBe("50MB");
      expect(config.logging.rotationSizeBytes).toBe(52428800);
    });

    test("logging.level 设置全局等级继承到 console 和 file", async () => {
      const configPath = join(tempDir, "logging-global-level.yaml");
      await writeFile(
        configPath,
        `server:
  logging:
    level: "debug"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
      );
      const config = await loadConfig(configPath);
      expect(config.logging.consoleLevel).toBe("debug");
      expect(config.logging.fileLevel).toBe("debug");
    });

    test("logging.console.level 覆盖全局等级", async () => {
      const configPath = join(tempDir, "logging-console-level.yaml");
      await writeFile(
        configPath,
        `server:
  logging:
    level: "warn"
    console:
      level: "trace"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
      );
      const config = await loadConfig(configPath);
      expect(config.logging.consoleLevel).toBe("trace");
      expect(config.logging.fileLevel).toBe("warn");
    });

    test("logging.file.level 独立覆盖", async () => {
      const configPath = join(tempDir, "logging-file-level.yaml");
      await writeFile(
        configPath,
        `server:
  logging:
    level: "info"
    file:
      level: "error"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
      );
      const config = await loadConfig(configPath);
      expect(config.logging.consoleLevel).toBe("info");
      expect(config.logging.fileLevel).toBe("error");
    });

    test("logging.file.path 绝对路径保持不变", async () => {
      const configPath = join(tempDir, "logging-abs-path.yaml");
      await writeFile(
        configPath,
        `server:
  logging:
    file:
      path: "/var/log/dial/app.log"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
      );
      const config = await loadConfig(configPath);
      expect(config.logging.filePath).toBe("/var/log/dial/app.log");
    });

    test("logging.file.path 相对路径基于配置文件目录解析", async () => {
      const configPath = join(tempDir, "logging-rel-path.yaml");
      await writeFile(
        configPath,
        `server:
  logging:
    file:
      path: "custom-logs/app.log"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
      );
      const config = await loadConfig(configPath);
      expect(config.logging.filePath).toBe(join(tempDir, "custom-logs/app.log"));
    });

    test("logging.file.rotation 自定义参数", async () => {
      const configPath = join(tempDir, "logging-rotation.yaml");
      await writeFile(
        configPath,
        `server:
  logging:
    file:
      rotation:
        size: "100MB"
        frequency: "hourly"
        maxFiles: 30
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
      );
      const config = await loadConfig(configPath);
      expect(config.logging.rotationSizeRaw).toBe("100MB");
      expect(config.logging.rotationSizeBytes).toBe(104857600);
      expect(config.logging.rotationFrequency).toBe("hourly");
      expect(config.logging.rotationMaxFiles).toBe(30);
    });

    test("logging.level 非法等级抛出错误", async () => {
      await expectConfigError(
        "logging-bad-level.yaml",
        `server:
  logging:
    level: "verbose"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "server.logging.level",
      );
    });

    test("logging.console.level 非法等级抛出错误", async () => {
      await expectConfigError(
        "logging-bad-console-level.yaml",
        `server:
  logging:
    console:
      level: "nope"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "server.logging.console.level",
      );
    });

    test("logging.file.level 非法等级抛出错误", async () => {
      await expectConfigError(
        "logging-bad-file-level.yaml",
        `server:
  logging:
    file:
      level: 123
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "server.logging.file.level",
      );
    });

    test("logging.file.rotation.size 非法格式抛出错误", async () => {
      await expectConfigError(
        "logging-bad-rotation-size.yaml",
        `server:
  logging:
    file:
      rotation:
        size: "100TB"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "无效的 size 格式",
      );
    });

    test("logging.file.rotation.frequency 非法值抛出错误", async () => {
      await expectConfigError(
        "logging-bad-frequency.yaml",
        `server:
  logging:
    file:
      rotation:
        frequency: "monthly"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "server.logging.file.rotation.frequency",
      );
    });

    test("logging.file.rotation.maxFiles 非整数抛出错误", async () => {
      await expectConfigError(
        "logging-bad-maxfiles.yaml",
        `server:
  logging:
    file:
      rotation:
        maxFiles: 3.5
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "server.logging.file.rotation.maxFiles",
      );
    });

    test("logging.file.path 空字符串抛出错误", async () => {
      await expectConfigError(
        "logging-empty-path.yaml",
        `server:
  logging:
    file:
      path: ""
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "server.logging.file.path",
      );
    });
  });

  describe("旧路径拒绝", () => {
    test("顶层 runtime 段应被拒绝为未知字段", async () => {
      await expectConfigError(
        "legacy-runtime.yaml",
        `runtime:
  maxConcurrentChecks: 10
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "runtime 是未知字段",
      );
    });

    test("顶层 logging 段应被拒绝为未知字段", async () => {
      await expectConfigError(
        "legacy-logging.yaml",
        `logging:
  level: "info"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "logging 是未知字段",
      );
    });

    test("server.host 应被拒绝为未知字段", async () => {
      await expectConfigError(
        "legacy-server-host.yaml",
        `server:
  host: "0.0.0.0"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "host 是未知字段",
      );
    });

    test("server.port 应被拒绝为未知字段", async () => {
      await expectConfigError(
        "legacy-server-port.yaml",
        `server:
  port: 8080
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "port 是未知字段",
      );
    });

    test("server.dataDir 应被拒绝为未知字段", async () => {
      await expectConfigError(
        "legacy-server-datadir.yaml",
        `server:
  dataDir: "/tmp/data"
targets:
  - id: "t"
    type: http
    http:
      url: "http://example.com"
`,
        "dataDir 是未知字段",
      );
    });
  });
});
