import { ConfigService } from "@nestjs/config";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { CIOperationsService } from "./ci-operations.service";
import {
  CIOperationError,
  FailureCategory,
  PipelineStatus,
  type PipelineWaitOptions,
} from "./types";

// Create mockExecAsync with vi.hoisted to make it available in vi.mock
const { mockExecAsync } = vi.hoisted(() => ({
  mockExecAsync: vi.fn(),
}));

// Mock node:util promisify
vi.mock("node:util", () => ({
  promisify: vi.fn(() => mockExecAsync),
}));

describe("CIOperationsService", () => {
  let service: CIOperationsService;
  let mockConfigService: ConfigService;

  beforeEach(() => {
    // Reset all mocks
    vi.clearAllMocks();

    // Create mock config service
    mockConfigService = {
      get: vi.fn((key: string, defaultValue?: unknown) => {
        if (key === "orchestrator.ci.woodpeckerServer") return "https://ci.example.com";
        if (key === "orchestrator.ci.woodpeckerToken") return "test-token";
        if (key === "orchestrator.ci.timeout") return defaultValue ?? 1800;
        if (key === "orchestrator.ci.pollInterval") return defaultValue ?? 10;
        return undefined;
      }),
    } as unknown as ConfigService;

    // Create service with mock
    service = new CIOperationsService(mockConfigService);
  });

  describe("Configuration", () => {
    it("should initialize with config service values", () => {
      expect(mockConfigService.get).toHaveBeenCalledWith("orchestrator.ci.woodpeckerServer");
      expect(mockConfigService.get).toHaveBeenCalledWith("orchestrator.ci.woodpeckerToken");
      expect(service.isConfigured()).toBe(true);
    });

    it("should fall back to environment variables if config not set", () => {
      const configWithoutCI = {
        get: vi.fn(() => undefined),
      } as unknown as ConfigService;

      process.env.WOODPECKER_SERVER = "https://ci.env.com";
      process.env.WOODPECKER_TOKEN = "env-token";

      const envService = new CIOperationsService(configWithoutCI);
      expect(envService.isConfigured()).toBe(true);

      // Clean up
      delete process.env.WOODPECKER_SERVER;
      delete process.env.WOODPECKER_TOKEN;
    });

    it("should not be configured when missing server", () => {
      const noServerConfig = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          if (key === "orchestrator.ci.woodpeckerServer") return "";
          if (key === "orchestrator.ci.woodpeckerToken") return "test-token";
          if (key === "orchestrator.ci.timeout") return defaultValue ?? 1800;
          if (key === "orchestrator.ci.pollInterval") return defaultValue ?? 10;
          return undefined;
        }),
      } as unknown as ConfigService;

      const noServerService = new CIOperationsService(noServerConfig);
      expect(noServerService.isConfigured()).toBe(false);
    });

    it("should not be configured when missing token", () => {
      const noTokenConfig = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          if (key === "orchestrator.ci.woodpeckerServer") return "https://ci.example.com";
          if (key === "orchestrator.ci.woodpeckerToken") return "";
          if (key === "orchestrator.ci.timeout") return defaultValue ?? 1800;
          if (key === "orchestrator.ci.pollInterval") return defaultValue ?? 10;
          return undefined;
        }),
      } as unknown as ConfigService;

      const noTokenService = new CIOperationsService(noTokenConfig);
      expect(noTokenService.isConfigured()).toBe(false);
    });

    it("should use default timeout and poll interval", () => {
      const minimalConfig = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          if (key === "orchestrator.ci.woodpeckerServer") return "https://ci.example.com";
          if (key === "orchestrator.ci.woodpeckerToken") return "test-token";
          return defaultValue;
        }),
      } as unknown as ConfigService;

      const minimalService = new CIOperationsService(minimalConfig);
      expect(minimalService.isConfigured()).toBe(true);
      // Defaults are verified through behavior in other tests
    });
  });

  describe("isConfigured", () => {
    it("should return true when both server and token are set", () => {
      expect(service.isConfigured()).toBe(true);
    });

    it("should return false when not configured", () => {
      const emptyConfig = {
        get: vi.fn(() => ""),
      } as unknown as ConfigService;

      const emptyService = new CIOperationsService(emptyConfig);
      expect(emptyService.isConfigured()).toBe(false);
    });
  });

  describe("getLatestPipeline", () => {
    it("should throw error when not configured", async () => {
      const unconfiguredConfig = {
        get: vi.fn(() => ""),
      } as unknown as ConfigService;

      const unconfiguredService = new CIOperationsService(unconfiguredConfig);

      await expect(unconfiguredService.getLatestPipeline("owner/repo")).rejects.toThrow(
        CIOperationError
      );

      try {
        await unconfiguredService.getLatestPipeline("owner/repo");
      } catch (e) {
        expect(e).toBeInstanceOf(CIOperationError);
        expect((e as CIOperationError).message).toBe("Woodpecker CI is not configured");
        expect((e as CIOperationError).operation).toBe("getLatestPipeline");
      }
    });

    it("should return latest pipeline successfully", async () => {
      const mockOutput = `Number | Status  | Event | Branch  | Commit   | Author
123    | success | push  | main    | abc123   | user`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getLatestPipeline("owner/repo");

      expect(mockExecAsync).toHaveBeenCalledWith("woodpecker pipeline ls owner/repo --limit 1", {
        env: expect.objectContaining({
          WOODPECKER_SERVER: "https://ci.example.com",
          WOODPECKER_TOKEN: "test-token",
        }),
      });
      expect(result).toEqual({
        number: 123,
        status: PipelineStatus.SUCCESS,
        event: "push",
        branch: "main",
        commit: "abc123",
      });
    });

    it("should return null when no pipelines found", async () => {
      const mockOutput = `Number | Status  | Event | Branch  | Commit   | Author`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getLatestPipeline("owner/repo");

      expect(result).toBeNull();
    });

    it("should handle empty output", async () => {
      mockExecAsync.mockResolvedValue({ stdout: "", stderr: "" });

      const result = await service.getLatestPipeline("owner/repo");

      expect(result).toBeNull();
    });

    it("should throw CIOperationError on exec failure", async () => {
      const execError = new Error("Command failed");
      mockExecAsync.mockRejectedValue(execError);

      await expect(service.getLatestPipeline("owner/repo")).rejects.toThrow(CIOperationError);

      try {
        await service.getLatestPipeline("owner/repo");
      } catch (e) {
        expect(e).toBeInstanceOf(CIOperationError);
        expect((e as CIOperationError).message).toContain("Failed to get latest pipeline");
        expect((e as CIOperationError).operation).toBe("getLatestPipeline");
        expect((e as CIOperationError).cause).toBe(execError);
      }
    });
  });

  describe("getPipeline", () => {
    it("should throw error when not configured", async () => {
      const unconfiguredConfig = {
        get: vi.fn(() => ""),
      } as unknown as ConfigService;

      const unconfiguredService = new CIOperationsService(unconfiguredConfig);

      await expect(unconfiguredService.getPipeline("owner/repo", 123)).rejects.toThrow(
        CIOperationError
      );

      try {
        await unconfiguredService.getPipeline("owner/repo", 123);
      } catch (e) {
        expect(e).toBeInstanceOf(CIOperationError);
        expect((e as CIOperationError).message).toBe("Woodpecker CI is not configured");
        expect((e as CIOperationError).operation).toBe("getPipeline");
      }
    });

    it("should get specific pipeline successfully", async () => {
      const mockOutput = `Number: 123
Status: success
Event: push
Branch: main
Commit: abc123
Started: Mon Jan 01 2024 10:00:00 GMT+0000
Finished: Mon Jan 01 2024 10:05:00 GMT+0000`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getPipeline("owner/repo", 123);

      expect(mockExecAsync).toHaveBeenCalledWith("woodpecker pipeline info owner/repo 123", {
        env: expect.objectContaining({
          WOODPECKER_SERVER: "https://ci.example.com",
          WOODPECKER_TOKEN: "test-token",
        }),
      });
      expect(result).toEqual({
        number: 123,
        status: PipelineStatus.SUCCESS,
        event: "push",
        branch: "main",
        commit: "abc123",
        started: expect.any(Number),
        finished: expect.any(Number),
        error: undefined,
      });
      expect(result.started).toBeGreaterThan(0);
      expect(result.finished).toBeGreaterThan(0);
    });

    it("should handle pipeline with error message", async () => {
      const mockOutput = `Number: 123
Status: failure
Event: push
Branch: main
Commit: abc123
Error: Build failed`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getPipeline("owner/repo", 123);

      expect(result).toEqual({
        number: 123,
        status: PipelineStatus.FAILURE,
        event: "push",
        branch: "main",
        commit: "abc123",
        error: "Build failed",
      });
    });

    it("should handle pipeline info with minimal fields", async () => {
      const mockOutput = `Status: running`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getPipeline("owner/repo", 456);

      expect(result).toEqual({
        number: 456,
        status: PipelineStatus.RUNNING,
        event: "",
        branch: "",
        commit: "",
      });
    });

    it("should throw CIOperationError on exec failure", async () => {
      const execError = new Error("Command failed");
      mockExecAsync.mockRejectedValue(execError);

      await expect(service.getPipeline("owner/repo", 123)).rejects.toThrow(CIOperationError);

      try {
        await service.getPipeline("owner/repo", 123);
      } catch (e) {
        expect(e).toBeInstanceOf(CIOperationError);
        expect((e as CIOperationError).message).toContain("Failed to get pipeline #123");
        expect((e as CIOperationError).operation).toBe("getPipeline");
        expect((e as CIOperationError).cause).toBe(execError);
      }
    });
  });

  describe("waitForPipeline", () => {
    it("should throw error when not configured", async () => {
      const unconfiguredConfig = {
        get: vi.fn(() => ""),
      } as unknown as ConfigService;

      const unconfiguredService = new CIOperationsService(unconfiguredConfig);

      await expect(unconfiguredService.waitForPipeline("owner/repo", 123)).rejects.toThrow(
        CIOperationError
      );

      try {
        await unconfiguredService.waitForPipeline("owner/repo", 123);
      } catch (e) {
        expect(e).toBeInstanceOf(CIOperationError);
        expect((e as CIOperationError).message).toBe("Woodpecker CI is not configured");
        expect((e as CIOperationError).operation).toBe("waitForPipeline");
      }
    });

    it("should return success when pipeline succeeds immediately", async () => {
      const mockOutput = `Number: 123
Status: success
Event: push
Branch: main
Commit: abc123`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result).toEqual({
        success: true,
        pipeline: {
          number: 123,
          status: PipelineStatus.SUCCESS,
          event: "push",
          branch: "main",
          commit: "abc123",
        },
      });
      expect(mockExecAsync).toHaveBeenCalledTimes(1);
    });

    it("should poll until pipeline succeeds", async () => {
      const runningOutput = `Number: 123
Status: running
Event: push
Branch: main
Commit: abc123`;

      const successOutput = `Number: 123
Status: success
Event: push
Branch: main
Commit: abc123`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: runningOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: runningOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: successOutput, stderr: "" });

      const options: PipelineWaitOptions = {
        timeout: 60,
        pollInterval: 0.001, // 1ms for testing
      };

      const result = await service.waitForPipeline("owner/repo", 123, options);

      expect(result.success).toBe(true);
      expect(mockExecAsync).toHaveBeenCalledTimes(3);
    });

    it("should fetch logs and diagnose on failure", async () => {
      const failureOutput = `Number: 123
Status: failure
Event: push
Branch: main
Commit: abc123`;

      const logsOutput = `Error: eslint errors found
src/test.ts:10:5 - error: Missing semicolon`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: failureOutput, stderr: "" }) // getPipeline
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" }); // getPipelineLogs

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.success).toBe(false);
      expect(result.logs).toBe(logsOutput);
      expect(result.diagnosis).toEqual({
        category: FailureCategory.LINT,
        message: "Linting errors detected",
        suggestion: "Run 'pnpm lint' locally to identify and fix linting issues",
        details: expect.stringContaining("error"),
      });
      expect(mockExecAsync).toHaveBeenCalledWith("woodpecker log show owner/repo 123", {
        env: expect.anything(),
      });
    });

    it("should not fetch logs when fetchLogsOnFailure is false", async () => {
      const failureOutput = `Number: 123
Status: failure
Event: push
Branch: main
Commit: abc123`;

      mockExecAsync.mockResolvedValue({ stdout: failureOutput, stderr: "" });

      const options: PipelineWaitOptions = {
        fetchLogsOnFailure: false,
      };

      const result = await service.waitForPipeline("owner/repo", 123, options);

      expect(result.success).toBe(false);
      expect(result.logs).toBeUndefined();
      expect(result.diagnosis).toBeUndefined();
      expect(mockExecAsync).toHaveBeenCalledTimes(1); // Only getPipeline, no logs
    });

    it("should handle killed status", async () => {
      const killedOutput = `Number: 123
Status: killed
Event: push
Branch: main
Commit: abc123`;

      const logsOutput = `Pipeline cancelled by user`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: killedOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.success).toBe(false);
      expect(result.pipeline.status).toBe(PipelineStatus.KILLED);
    });

    it("should handle error status", async () => {
      const errorOutput = `Number: 123
Status: error
Event: push
Branch: main
Commit: abc123`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: errorOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: "Error log", stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.success).toBe(false);
      expect(result.pipeline.status).toBe(PipelineStatus.ERROR);
    });

    it("should timeout when pipeline does not complete", async () => {
      const runningOutput = `Number: 123
Status: running
Event: push
Branch: main
Commit: abc123`;

      mockExecAsync.mockResolvedValue({ stdout: runningOutput, stderr: "" });

      const options: PipelineWaitOptions = {
        timeout: 0.01, // 10ms
        pollInterval: 0.001, // 1ms
      };

      await expect(service.waitForPipeline("owner/repo", 123, options)).rejects.toThrow(
        CIOperationError
      );

      try {
        await service.waitForPipeline("owner/repo", 123, options);
      } catch (e) {
        expect(e).toBeInstanceOf(CIOperationError);
        expect((e as CIOperationError).message).toContain("did not complete within");
        expect((e as CIOperationError).message).toContain("status: running");
        expect((e as CIOperationError).operation).toBe("waitForPipeline");
      }
    });

    it("should use custom timeout and poll interval", async () => {
      const successOutput = `Number: 123
Status: success
Event: push
Branch: main
Commit: abc123`;

      mockExecAsync.mockResolvedValue({ stdout: successOutput, stderr: "" });

      const options: PipelineWaitOptions = {
        timeout: 3600, // 1 hour
        pollInterval: 5, // 5 seconds
      };

      const result = await service.waitForPipeline("owner/repo", 123, options);

      expect(result.success).toBe(true);
    });
  });

  describe("getPipelineLogs", () => {
    it("should throw error when not configured", async () => {
      const unconfiguredConfig = {
        get: vi.fn(() => ""),
      } as unknown as ConfigService;

      const unconfiguredService = new CIOperationsService(unconfiguredConfig);

      await expect(unconfiguredService.getPipelineLogs("owner/repo", 123)).rejects.toThrow(
        CIOperationError
      );

      try {
        await unconfiguredService.getPipelineLogs("owner/repo", 123);
      } catch (e) {
        expect(e).toBeInstanceOf(CIOperationError);
        expect((e as CIOperationError).message).toBe("Woodpecker CI is not configured");
        expect((e as CIOperationError).operation).toBe("getPipelineLogs");
      }
    });

    it("should get pipeline logs successfully", async () => {
      const mockLogs = `Step 1: Build
Building project...
Step 2: Test
Running tests...`;

      mockExecAsync.mockResolvedValue({ stdout: mockLogs, stderr: "" });

      const result = await service.getPipelineLogs("owner/repo", 123);

      expect(mockExecAsync).toHaveBeenCalledWith("woodpecker log show owner/repo 123", {
        env: expect.objectContaining({
          WOODPECKER_SERVER: "https://ci.example.com",
          WOODPECKER_TOKEN: "test-token",
        }),
      });
      expect(result).toBe(mockLogs);
    });

    it("should get logs for specific step", async () => {
      const mockLogs = `Step 2: Test
Running tests...
Test passed`;

      mockExecAsync.mockResolvedValue({ stdout: mockLogs, stderr: "" });

      const result = await service.getPipelineLogs("owner/repo", 123, "test");

      expect(mockExecAsync).toHaveBeenCalledWith("woodpecker log show owner/repo 123 test", {
        env: expect.anything(),
      });
      expect(result).toBe(mockLogs);
    });

    it("should throw CIOperationError on exec failure", async () => {
      const execError = new Error("Command failed");
      mockExecAsync.mockRejectedValue(execError);

      await expect(service.getPipelineLogs("owner/repo", 123)).rejects.toThrow(CIOperationError);

      try {
        await service.getPipelineLogs("owner/repo", 123);
      } catch (e) {
        expect(e).toBeInstanceOf(CIOperationError);
        expect((e as CIOperationError).message).toContain("Failed to get logs");
        expect((e as CIOperationError).operation).toBe("getPipelineLogs");
        expect((e as CIOperationError).cause).toBe(execError);
      }
    });
  });

  describe("parsePipelineList", () => {
    it("should parse pipeline list with multiple entries", async () => {
      const mockOutput = `Number | Status  | Event | Branch  | Commit   | Author
123    | success | push  | main    | abc123   | user1
122    | failure | push  | develop | def456   | user2
121    | running | push  | main    | ghi789   | user3`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getLatestPipeline("owner/repo");

      // We get only the first one due to --limit 1, but parsing should handle multiple
      expect(result).toBeDefined();
      expect(result?.number).toBe(123);
    });

    it("should parse pipeline list with different statuses", async () => {
      const mockOutput = `Number | Status   | Event | Branch  | Commit   | Author
123    | pending  | push  | main    | abc123   | user1`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getLatestPipeline("owner/repo");

      expect(result?.status).toBe(PipelineStatus.PENDING);
    });

    it("should handle malformed lines in pipeline list", async () => {
      const mockOutput = `Number | Status  | Event | Branch  | Commit   | Author
123    | success | push  | main    | abc123   | user1
invalid line
not enough columns`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getLatestPipeline("owner/repo");

      expect(result).toBeDefined();
      expect(result?.number).toBe(123);
    });

    it("should handle non-numeric pipeline numbers", async () => {
      const mockOutput = `Number | Status  | Event | Branch  | Commit   | Author
abc    | success | push  | main    | abc123   | user1`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getLatestPipeline("owner/repo");

      expect(result).toBeNull(); // Invalid number should be filtered out
    });
  });

  describe("parsePipelineInfo", () => {
    it("should parse all status types correctly", async () => {
      const statuses = [
        { input: "success", expected: PipelineStatus.SUCCESS },
        { input: "failure", expected: PipelineStatus.FAILURE },
        { input: "failed", expected: PipelineStatus.FAILURE },
        { input: "running", expected: PipelineStatus.RUNNING },
        { input: "pending", expected: PipelineStatus.PENDING },
        { input: "killed", expected: PipelineStatus.KILLED },
        { input: "cancelled", expected: PipelineStatus.KILLED },
        { input: "error", expected: PipelineStatus.ERROR },
        { input: "skipped", expected: PipelineStatus.SKIPPED },
        { input: "blocked", expected: PipelineStatus.BLOCKED },
      ];

      for (const { input, expected } of statuses) {
        const mockOutput = `Status: ${input}`;
        mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

        const result = await service.getPipeline("owner/repo", 123);
        expect(result.status).toBe(expected);
      }
    });

    it("should handle unknown status", async () => {
      const mockOutput = `Status: unknownstatus`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getPipeline("owner/repo", 123);

      expect(result.status).toBe(PipelineStatus.PENDING); // Default to pending
    });

    it("should handle case-insensitive status", async () => {
      const mockOutput = `Status: SUCCESS`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getPipeline("owner/repo", 123);

      expect(result.status).toBe(PipelineStatus.SUCCESS);
    });

    it("should handle status with extra whitespace", async () => {
      const mockOutput = `Status:   success  `;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getPipeline("owner/repo", 123);

      expect(result.status).toBe(PipelineStatus.SUCCESS);
    });

    it("should parse timestamps correctly", async () => {
      const mockOutput = `Status: success
Started: Mon Jan 01 2024 10:00:00 GMT+0000
Finished: Mon Jan 01 2024 10:05:00 GMT+0000`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getPipeline("owner/repo", 123);

      expect(result.started).toBeGreaterThan(0);
      expect(result.finished).toBeGreaterThan(0);
      expect(Number.isNaN(result.started)).toBe(false);
      expect(Number.isNaN(result.finished)).toBe(false);
    });

    it("should handle invalid timestamps", async () => {
      const mockOutput = `Status: success
Started: invalid-date
Finished: also-invalid`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      const result = await service.getPipeline("owner/repo", 123);

      expect(isNaN(result.started as number)).toBe(true);
      expect(isNaN(result.finished as number)).toBe(true);
    });
  });

  describe("diagnoseFail", () => {
    it("should diagnose lint failures", async () => {
      const failureOutput = `Status: failure`;
      const logsOutput = `Running eslint...
error: Missing semicolon at line 10
error: Unused variable 'x' at line 20`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: failureOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.diagnosis?.category).toBe(FailureCategory.LINT);
      expect(result.diagnosis?.message).toBe("Linting errors detected");
      expect(result.diagnosis?.suggestion).toContain("pnpm lint");
    });

    it("should diagnose type check failures", async () => {
      const failureOutput = `Status: failure`;
      const logsOutput = `Running typescript compiler...
Type error: Argument of type 'string' is not assignable to parameter of type 'number'
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'
error TS2322: Type 'null' is not assignable to type 'string'`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: failureOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.diagnosis?.category).toBe(FailureCategory.TYPE_CHECK);
      expect(result.diagnosis?.message).toBe("TypeScript type errors detected");
      expect(result.diagnosis?.suggestion).toContain("pnpm typecheck");
      expect(result.diagnosis?.details).toContain("error TS2345");
    });

    it("should diagnose test failures", async () => {
      const failureOutput = `Status: failure`;
      const logsOutput = `Running tests...
FAIL src/test.spec.ts
  ✓ test 1 passed
  ✗ test 2 failed
  ✗ test 3 failed`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: failureOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.diagnosis?.category).toBe(FailureCategory.TEST);
      expect(result.diagnosis?.message).toBe("Test failures detected");
      expect(result.diagnosis?.suggestion).toContain("pnpm test");
      expect(result.diagnosis?.details).toContain("FAIL");
    });

    it("should diagnose build failures", async () => {
      const failureOutput = `Status: failure`;
      const logsOutput = `Building project...
error: Cannot find module 'missing-package'
build failed with errors`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: failureOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.diagnosis?.category).toBe(FailureCategory.BUILD);
      expect(result.diagnosis?.message).toBe("Build errors detected");
      expect(result.diagnosis?.suggestion).toContain("pnpm build");
    });

    it("should diagnose security failures", async () => {
      const failureOutput = `Status: failure`;
      const logsOutput = `Scanning for secrets...
Found hardcoded secret in file.ts
security check failed`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: failureOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.diagnosis?.category).toBe(FailureCategory.SECURITY);
      expect(result.diagnosis?.message).toBe("Security check failed");
      expect(result.diagnosis?.suggestion).toContain("remove any hardcoded secrets");
      expect(result.diagnosis?.details).toContain("secret");
    });

    it("should handle unknown failure type", async () => {
      const failureOutput = `Status: failure`;
      const logsOutput = `Some random error occurred
No pattern match`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: failureOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.diagnosis?.category).toBe(FailureCategory.UNKNOWN);
      expect(result.diagnosis?.message).toBe("Pipeline failed with unknown error");
      expect(result.diagnosis?.suggestion).toContain("Review full pipeline logs");
    });

    it("should extract and limit error details", async () => {
      const failureOutput = `Status: failure`;
      // Create logs with many errors
      const errors = Array.from({ length: 15 }, (_, i) => `error line ${i + 1}`).join("\n");
      const logsOutput = `Lint errors:\n${errors}`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: failureOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.diagnosis?.category).toBe(FailureCategory.LINT);
      // Should only include first 10 errors
      const detailLines = result.diagnosis?.details?.split("\n") || [];
      expect(detailLines.length).toBeLessThanOrEqual(10);
    });

    it("should handle empty error extraction", async () => {
      const failureOutput = `Status: failure`;
      const logsOutput = `Build stopped but no specific matches`;

      mockExecAsync
        .mockResolvedValueOnce({ stdout: failureOutput, stderr: "" })
        .mockResolvedValueOnce({ stdout: logsOutput, stderr: "" });

      const result = await service.waitForPipeline("owner/repo", 123);

      expect(result.diagnosis?.category).toBe(FailureCategory.UNKNOWN);
      expect(result.diagnosis?.details).toBeUndefined();
    });
  });

  describe("execWoodpecker", () => {
    it("should execute with correct environment variables", async () => {
      const mockOutput = `Number | Status | Event | Branch | Commit`;

      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      await service.getLatestPipeline("owner/repo");

      expect(mockExecAsync).toHaveBeenCalledWith("woodpecker pipeline ls owner/repo --limit 1", {
        env: expect.objectContaining({
          WOODPECKER_SERVER: "https://ci.example.com",
          WOODPECKER_TOKEN: "test-token",
        }),
      });
    });

    it("should include all process.env variables", async () => {
      const mockOutput = `Number | Status | Event | Branch | Commit`;

      process.env.CUSTOM_VAR = "test-value";
      mockExecAsync.mockResolvedValue({ stdout: mockOutput, stderr: "" });

      await service.getLatestPipeline("owner/repo");

      expect(mockExecAsync).toHaveBeenCalledWith("woodpecker pipeline ls owner/repo --limit 1", {
        env: expect.objectContaining({
          CUSTOM_VAR: "test-value",
        }),
      });

      delete process.env.CUSTOM_VAR;
    });

    it("should handle stderr in exec errors", async () => {
      const execError = Object.assign(new Error("Command failed"), {
        stderr: "Error: Repository not found",
      });
      mockExecAsync.mockRejectedValue(execError);

      await expect(service.getLatestPipeline("owner/repo")).rejects.toThrow();
    });

    it("should handle exec errors without stderr", async () => {
      const execError = new Error("Command failed");
      mockExecAsync.mockRejectedValue(execError);

      await expect(service.getLatestPipeline("owner/repo")).rejects.toThrow();
    });
  });
});
