import { describe, expect, it } from "vitest";
import { validate } from "class-validator";
import { plainToInstance } from "class-transformer";
import { SpawnAgentDto, AgentContextDto } from "./spawn-agent.dto";

/**
 * Builds a valid SpawnAgentDto plain object for use as a baseline.
 * Individual tests override specific fields to trigger validation failures.
 */
function validSpawnPayload(): Record<string, unknown> {
  return {
    taskId: "task-abc-123",
    agentType: "worker",
    context: {
      repository: "https://git.example.com/org/repo.git",
      branch: "feature/my-branch",
      workItems: ["US-001"],
    },
  };
}

describe("SpawnAgentDto validation", () => {
  // ------------------------------------------------------------------ //
  //  Happy path
  // ------------------------------------------------------------------ //
  it("should pass validation for a valid spawn request", async () => {
    const dto = plainToInstance(SpawnAgentDto, validSpawnPayload());
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it("should pass validation with optional gateProfile", async () => {
    const dto = plainToInstance(SpawnAgentDto, {
      ...validSpawnPayload(),
      gateProfile: "strict",
    });
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it("should pass validation with optional skills array", async () => {
    const payload = validSpawnPayload();
    (payload.context as Record<string, unknown>).skills = ["skill-a", "skill-b"];
    const dto = plainToInstance(SpawnAgentDto, payload);
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  // ------------------------------------------------------------------ //
  //  taskId validation
  // ------------------------------------------------------------------ //
  describe("taskId", () => {
    it("should reject missing taskId", async () => {
      const payload = validSpawnPayload();
      delete payload.taskId;
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
      const taskIdError = errors.find((e) => e.property === "taskId");
      expect(taskIdError).toBeDefined();
    });

    it("should reject empty-string taskId", async () => {
      const dto = plainToInstance(SpawnAgentDto, {
        ...validSpawnPayload(),
        taskId: "",
      });
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
      const taskIdError = errors.find((e) => e.property === "taskId");
      expect(taskIdError).toBeDefined();
    });
  });

  // ------------------------------------------------------------------ //
  //  agentType validation
  // ------------------------------------------------------------------ //
  describe("agentType", () => {
    it("should reject invalid agentType value", async () => {
      const dto = plainToInstance(SpawnAgentDto, {
        ...validSpawnPayload(),
        agentType: "hacker",
      });
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
      const agentTypeError = errors.find((e) => e.property === "agentType");
      expect(agentTypeError).toBeDefined();
    });

    it("should accept all valid agentType values", async () => {
      for (const validType of ["worker", "reviewer", "tester"]) {
        const dto = plainToInstance(SpawnAgentDto, {
          ...validSpawnPayload(),
          agentType: validType,
        });
        const errors = await validate(dto);
        expect(errors).toHaveLength(0);
      }
    });
  });

  // ------------------------------------------------------------------ //
  //  gateProfile validation
  // ------------------------------------------------------------------ //
  describe("gateProfile", () => {
    it("should reject invalid gateProfile value", async () => {
      const dto = plainToInstance(SpawnAgentDto, {
        ...validSpawnPayload(),
        gateProfile: "invalid-profile",
      });
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
      const gateError = errors.find((e) => e.property === "gateProfile");
      expect(gateError).toBeDefined();
    });

    it("should accept all valid gateProfile values", async () => {
      for (const profile of ["strict", "standard", "minimal", "custom"]) {
        const dto = plainToInstance(SpawnAgentDto, {
          ...validSpawnPayload(),
          gateProfile: profile,
        });
        const errors = await validate(dto);
        expect(errors).toHaveLength(0);
      }
    });
  });

  // ------------------------------------------------------------------ //
  //  Nested AgentContextDto validation
  // ------------------------------------------------------------------ //
  describe("context (nested AgentContextDto)", () => {
    // ------ repository ------ //
    it("should reject empty repository", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).repository = "";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject SSRF repository URL pointing to localhost", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).repository = "https://127.0.0.1/evil/repo.git";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject SSRF repository URL pointing to private network", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).repository =
        "https://192.168.1.100/org/repo.git";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject repository URL with file:// protocol", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).repository = "file:///etc/passwd";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject repository URL with dangerous characters", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).repository =
        "https://git.example.com/repo;rm -rf /";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    // ------ branch ------ //
    it("should reject empty branch", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).branch = "";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject shell injection in branch name via $(command)", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).branch = "$(rm -rf /)";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject shell injection in branch name via backticks", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).branch = "`whoami`";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject branch name with semicolon injection", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).branch = "main;cat /etc/passwd";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject branch name starting with hyphen (option injection)", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).branch = "--delete";
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    // ------ workItems ------ //
    it("should reject empty workItems array", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).workItems = [];
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject missing workItems", async () => {
      const payload = validSpawnPayload();
      delete (payload.context as Record<string, unknown>).workItems;
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    // ------ workItems MaxLength / ArrayMaxSize (SEC-ORCH-29) ------ //
    it("should reject workItems array exceeding max size of 50", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).workItems = Array.from(
        { length: 51 },
        (_, i) => `US-${String(i + 1).padStart(3, "0")}`
      );
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should accept workItems array at max size of 50", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).workItems = Array.from(
        { length: 50 },
        (_, i) => `US-${String(i + 1).padStart(3, "0")}`
      );
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors).toHaveLength(0);
    });

    it("should reject a work item string exceeding 2000 characters", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).workItems = ["x".repeat(2001)];
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should accept a work item string at exactly 2000 characters", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).workItems = ["x".repeat(2000)];
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors).toHaveLength(0);
    });

    // ------ skills MaxLength / ArrayMaxSize (SEC-ORCH-29) ------ //
    it("should reject skills array exceeding max size of 20", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).skills = Array.from(
        { length: 21 },
        (_, i) => `skill-${i}`
      );
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it("should reject a skill string exceeding 200 characters", async () => {
      const payload = validSpawnPayload();
      (payload.context as Record<string, unknown>).skills = ["s".repeat(201)];
      const dto = plainToInstance(SpawnAgentDto, payload);
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });
  });

  // ------------------------------------------------------------------ //
  //  Standalone AgentContextDto validation
  // ------------------------------------------------------------------ //
  describe("AgentContextDto standalone", () => {
    it("should pass validation for a valid context", async () => {
      const dto = plainToInstance(AgentContextDto, {
        repository: "https://git.example.com/org/repo.git",
        branch: "feature/my-branch",
        workItems: ["US-001", "US-002"],
      });
      const errors = await validate(dto);
      expect(errors).toHaveLength(0);
    });

    it("should reject non-string items in workItems", async () => {
      const dto = plainToInstance(AgentContextDto, {
        repository: "https://git.example.com/org/repo.git",
        branch: "main",
        workItems: [123, true],
      });
      const errors = await validate(dto);
      expect(errors.length).toBeGreaterThan(0);
    });
  });
});
