import { ConfigService } from "@nestjs/config";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { CoordinatorClientService } from "./coordinator-client.service";

describe("CoordinatorClientService", () => {
  let service: CoordinatorClientService;
  let mockConfigService: ConfigService;
  const mockCoordinatorUrl = "http://localhost:8000";
  const mockApiKey = "test-api-key-12345";

  // Valid request for testing
  const validQualityCheckRequest = {
    taskId: "task-123",
    agentId: "agent-456",
    files: ["src/test.ts", "src/test.spec.ts"],
    diffSummary: "Added new test file",
  };

  // Mock fetch globally
  const mockFetch = vi.fn();
  global.fetch = mockFetch as unknown as typeof fetch;

  // Mock logger to capture warnings
  const mockLoggerWarn = vi.fn();
  const mockLoggerDebug = vi.fn();

  beforeEach(() => {
    vi.clearAllMocks();

    mockConfigService = {
      get: vi.fn((key: string, defaultValue?: unknown) => {
        if (key === "orchestrator.coordinator.url") return mockCoordinatorUrl;
        if (key === "orchestrator.coordinator.timeout") return 30000;
        if (key === "orchestrator.coordinator.retries") return 3;
        if (key === "orchestrator.coordinator.apiKey") return undefined;
        if (key === "NODE_ENV") return "development";
        return defaultValue;
      }),
    } as unknown as ConfigService;

    service = new CoordinatorClientService(mockConfigService);
  });

  it("should be defined", () => {
    expect(service).toBeDefined();
  });

  describe("checkQuality", () => {
    const qualityCheckRequest = validQualityCheckRequest;

    it("should successfully call quality check endpoint and return approved result", async () => {
      const mockResponse = {
        approved: true,
        gate: "all",
        message: "All quality gates passed",
        details: { build: "passed", lint: "passed", test: "passed" },
      };

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const result = await service.checkQuality(qualityCheckRequest);

      expect(mockFetch).toHaveBeenCalledWith(
        `${mockCoordinatorUrl}/api/quality/check`,
        expect.objectContaining({
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(qualityCheckRequest),
        })
      );

      expect(result).toEqual(mockResponse);
      expect(result.approved).toBe(true);
    });

    it("should successfully call quality check endpoint and return rejected result", async () => {
      const mockResponse = {
        approved: false,
        gate: "lint",
        message: "Linting failed",
        details: { errors: ["Unexpected any type"], file: "src/test.ts" },
      };

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const result = await service.checkQuality(qualityCheckRequest);

      expect(result).toEqual(mockResponse);
      expect(result.approved).toBe(false);
      expect(result.gate).toBe("lint");
    });

    it("should throw error when coordinator returns non-200 status", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        statusText: "Internal Server Error",
      });

      await expect(service.checkQuality(qualityCheckRequest)).rejects.toThrow(
        "Coordinator quality check failed: 500 Internal Server Error"
      );
    });

    it("should retry on network error and succeed on second attempt", async () => {
      const mockResponse = {
        approved: true,
        gate: "all",
        message: "All quality gates passed",
      };

      // First call fails with network error
      mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));

      // Second call succeeds
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const result = await service.checkQuality(qualityCheckRequest);

      expect(mockFetch).toHaveBeenCalledTimes(2);
      expect(result).toEqual(mockResponse);
    });

    it("should retry on coordinator unavailable (503) and succeed", async () => {
      const mockResponse = {
        approved: true,
        gate: "all",
        message: "All quality gates passed",
      };

      // First call returns 503
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 503,
        statusText: "Service Unavailable",
      });

      // Second call succeeds
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const result = await service.checkQuality(qualityCheckRequest);

      expect(mockFetch).toHaveBeenCalledTimes(2);
      expect(result).toEqual(mockResponse);
    });

    it("should fail after max retries exceeded", async () => {
      // All 3 retries fail
      mockFetch.mockRejectedValue(new Error("ECONNREFUSED"));

      await expect(service.checkQuality(qualityCheckRequest)).rejects.toThrow("ECONNREFUSED");

      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it("should fail after max retries on 503 errors", async () => {
      // All 3 retries return 503
      mockFetch.mockResolvedValue({
        ok: false,
        status: 503,
        statusText: "Service Unavailable",
      });

      await expect(service.checkQuality(qualityCheckRequest)).rejects.toThrow(
        "Coordinator quality check failed: 503 Service Unavailable"
      );

      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it("should throw error on invalid JSON response", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => {
          throw new Error("Invalid JSON");
        },
      });

      await expect(service.checkQuality(qualityCheckRequest)).rejects.toThrow(
        "Failed to parse coordinator response"
      );
    });

    it("should handle timeout", async () => {
      // Mock a timeout scenario
      mockFetch.mockImplementationOnce(
        () => new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 100))
      );

      await expect(service.checkQuality(qualityCheckRequest)).rejects.toThrow();
    });

    it("should validate response structure", async () => {
      const invalidResponse = {
        // Missing required 'approved' field
        gate: "all",
        message: "Test",
      };

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => invalidResponse,
      });

      await expect(service.checkQuality(qualityCheckRequest)).rejects.toThrow(
        "Invalid coordinator response"
      );
    });

    it("should reject null response", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => null,
      });

      await expect(service.checkQuality(qualityCheckRequest)).rejects.toThrow(
        "Invalid coordinator response"
      );
    });
  });

  describe("isHealthy", () => {
    it("should return true when coordinator health check succeeds", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ status: "healthy" }),
      });

      const result = await service.isHealthy();

      expect(mockFetch).toHaveBeenCalledWith(
        `${mockCoordinatorUrl}/health`,
        expect.objectContaining({
          signal: expect.any(Object),
        })
      );
      expect(result).toBe(true);
    });

    it("should return false when coordinator health check fails", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 503,
      });

      const result = await service.isHealthy();

      expect(result).toBe(false);
    });

    it("should return false on network error", async () => {
      mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));

      const result = await service.isHealthy();

      expect(result).toBe(false);
    });
  });

  describe("input validation", () => {
    it("should reject request with too many files (> 1000)", async () => {
      const files = Array(1001).fill("src/file.ts");
      const request = { ...validQualityCheckRequest, files };

      await expect(service.checkQuality(request)).rejects.toThrow(
        "files array exceeds maximum size of 1000"
      );
    });

    it("should accept request with exactly 1000 files", async () => {
      const files = Array(1000).fill("src/file.ts");
      const request = { ...validQualityCheckRequest, files };
      const mockResponse = {
        approved: true,
        gate: "all",
        message: "All quality gates passed",
      };

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const result = await service.checkQuality(request);
      expect(result).toEqual(mockResponse);
    });

    it("should reject file paths with path traversal attempts", async () => {
      const request = {
        ...validQualityCheckRequest,
        files: ["src/test.ts", "../../../etc/passwd"],
      };

      await expect(service.checkQuality(request)).rejects.toThrow(
        "file path contains path traversal"
      );
    });

    it("should reject file paths with null bytes", async () => {
      const request = {
        ...validQualityCheckRequest,
        files: ["src/test.ts", "src/file\0.ts"],
      };

      await expect(service.checkQuality(request)).rejects.toThrow(
        "file path contains invalid characters"
      );
    });

    it("should reject diff summary exceeding 10KB", async () => {
      const largeDiff = "x".repeat(10 * 1024 + 1); // 10KB + 1 byte
      const request = { ...validQualityCheckRequest, diffSummary: largeDiff };

      await expect(service.checkQuality(request)).rejects.toThrow(
        "diffSummary exceeds maximum size of 10KB"
      );
    });

    it("should accept diff summary of exactly 10KB", async () => {
      const largeDiff = "x".repeat(10 * 1024); // Exactly 10KB
      const request = { ...validQualityCheckRequest, diffSummary: largeDiff };
      const mockResponse = {
        approved: true,
        gate: "all",
        message: "All quality gates passed",
      };

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const result = await service.checkQuality(request);
      expect(result).toEqual(mockResponse);
    });

    it("should reject invalid taskId format", async () => {
      const request = { ...validQualityCheckRequest, taskId: "" };

      await expect(service.checkQuality(request)).rejects.toThrow("taskId cannot be empty");
    });

    it("should reject invalid agentId format", async () => {
      const request = { ...validQualityCheckRequest, agentId: "" };

      await expect(service.checkQuality(request)).rejects.toThrow("agentId cannot be empty");
    });

    it("should reject empty files array", async () => {
      const request = { ...validQualityCheckRequest, files: [] };

      await expect(service.checkQuality(request)).rejects.toThrow("files array cannot be empty");
    });

    it("should reject absolute file paths", async () => {
      const request = {
        ...validQualityCheckRequest,
        files: ["/etc/passwd", "src/file.ts"],
      };

      await expect(service.checkQuality(request)).rejects.toThrow("file path must be relative");
    });
  });

  describe("API key authentication", () => {
    it("should include X-API-Key header when API key is configured", async () => {
      const configWithApiKey = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          if (key === "orchestrator.coordinator.url") return mockCoordinatorUrl;
          if (key === "orchestrator.coordinator.timeout") return 30000;
          if (key === "orchestrator.coordinator.retries") return 3;
          if (key === "orchestrator.coordinator.apiKey") return mockApiKey;
          if (key === "NODE_ENV") return "development";
          return defaultValue;
        }),
      } as unknown as ConfigService;

      const serviceWithApiKey = new CoordinatorClientService(configWithApiKey);

      const mockResponse = {
        approved: true,
        gate: "all",
        message: "All quality gates passed",
      };

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      await serviceWithApiKey.checkQuality(validQualityCheckRequest);

      expect(mockFetch).toHaveBeenCalledWith(
        `${mockCoordinatorUrl}/api/quality/check`,
        expect.objectContaining({
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-API-Key": mockApiKey,
          },
          body: JSON.stringify(validQualityCheckRequest),
        })
      );
    });

    it("should not include X-API-Key header when API key is not configured", async () => {
      const mockResponse = {
        approved: true,
        gate: "all",
        message: "All quality gates passed",
      };

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      await service.checkQuality(validQualityCheckRequest);

      expect(mockFetch).toHaveBeenCalledWith(
        `${mockCoordinatorUrl}/api/quality/check`,
        expect.objectContaining({
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(validQualityCheckRequest),
        })
      );
    });

    it("should include X-API-Key header in health check when configured", async () => {
      const configWithApiKey = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          if (key === "orchestrator.coordinator.url") return mockCoordinatorUrl;
          if (key === "orchestrator.coordinator.timeout") return 30000;
          if (key === "orchestrator.coordinator.retries") return 3;
          if (key === "orchestrator.coordinator.apiKey") return mockApiKey;
          if (key === "NODE_ENV") return "development";
          return defaultValue;
        }),
      } as unknown as ConfigService;

      const serviceWithApiKey = new CoordinatorClientService(configWithApiKey);

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ status: "healthy" }),
      });

      await serviceWithApiKey.isHealthy();

      expect(mockFetch).toHaveBeenCalledWith(
        `${mockCoordinatorUrl}/health`,
        expect.objectContaining({
          headers: {
            "Content-Type": "application/json",
            "X-API-Key": mockApiKey,
          },
        })
      );
    });
  });

  describe("security warnings", () => {
    it("should log warning when API key is not configured in production", () => {
      const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);

      const configProduction = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          if (key === "orchestrator.coordinator.url") return mockCoordinatorUrl;
          if (key === "orchestrator.coordinator.timeout") return 30000;
          if (key === "orchestrator.coordinator.retries") return 3;
          if (key === "orchestrator.coordinator.apiKey") return undefined;
          if (key === "NODE_ENV") return "production";
          return defaultValue;
        }),
      } as unknown as ConfigService;

      // Creating service should trigger warnings - we can't directly test Logger.warn
      // but we can verify the service initializes without throwing
      const productionService = new CoordinatorClientService(configProduction);
      expect(productionService).toBeDefined();

      warnSpy.mockRestore();
    });

    it("should log warning when coordinator URL uses HTTP in production", () => {
      const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);

      const configProduction = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          if (key === "orchestrator.coordinator.url") return "http://coordinator.example.com";
          if (key === "orchestrator.coordinator.timeout") return 30000;
          if (key === "orchestrator.coordinator.retries") return 3;
          if (key === "orchestrator.coordinator.apiKey") return mockApiKey;
          if (key === "NODE_ENV") return "production";
          return defaultValue;
        }),
      } as unknown as ConfigService;

      // Creating service should trigger HTTPS warning
      const productionService = new CoordinatorClientService(configProduction);
      expect(productionService).toBeDefined();

      warnSpy.mockRestore();
    });

    it("should not log warnings when properly configured in production", () => {
      const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);

      const configProduction = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          if (key === "orchestrator.coordinator.url") return "https://coordinator.example.com";
          if (key === "orchestrator.coordinator.timeout") return 30000;
          if (key === "orchestrator.coordinator.retries") return 3;
          if (key === "orchestrator.coordinator.apiKey") return mockApiKey;
          if (key === "NODE_ENV") return "production";
          return defaultValue;
        }),
      } as unknown as ConfigService;

      // Creating service with proper config should not trigger warnings
      const productionService = new CoordinatorClientService(configProduction);
      expect(productionService).toBeDefined();

      warnSpy.mockRestore();
    });
  });
});
