import { describe, it, expect, beforeEach, vi } from "vitest";
import { ConfigService } from "@nestjs/config";
import { ValkeyService } from "./valkey.service";
import type { TaskState, AgentState, OrchestratorEvent } from "./types";

// Create mock client methods that will be shared
const mockClient = {
  getTaskState: vi.fn(),
  setTaskState: vi.fn(),
  deleteTaskState: vi.fn(),
  updateTaskStatus: vi.fn(),
  listTasks: vi.fn(),
  getAgentState: vi.fn(),
  setAgentState: vi.fn(),
  deleteAgentState: vi.fn(),
  updateAgentStatus: vi.fn(),
  listAgents: vi.fn(),
  publishEvent: vi.fn(),
  subscribeToEvents: vi.fn(),
  disconnect: vi.fn(),
};

// Mock ValkeyClient before importing
vi.mock("./valkey.client", () => {
  return {
    ValkeyClient: class {
      constructor() {
        return mockClient;
      }
    },
  };
});

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

  beforeEach(() => {
    // Clear all mock calls
    vi.clearAllMocks();

    // Create mock config service
    mockConfigService = {
      get: vi.fn((key: string, defaultValue?: unknown) => {
        const config: Record<string, unknown> = {
          "orchestrator.valkey.host": "localhost",
          "orchestrator.valkey.port": 6379,
        };
        return config[key] ?? defaultValue;
      }),
    } as unknown as ConfigService;

    // Create service directly
    service = new ValkeyService(mockConfigService);
  });

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

    it("should create ValkeyClient with config from ConfigService", () => {
      expect(mockConfigService.get).toHaveBeenCalledWith("orchestrator.valkey.host", "localhost");
      expect(mockConfigService.get).toHaveBeenCalledWith("orchestrator.valkey.port", 6379);
    });

    it("should use password from config if provided", () => {
      const configWithPassword = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          const config: Record<string, unknown> = {
            "orchestrator.valkey.host": "localhost",
            "orchestrator.valkey.port": 6379,
            "orchestrator.valkey.password": "secret",
          };
          return config[key] ?? defaultValue;
        }),
      } as unknown as ConfigService;

      const serviceWithPassword = new ValkeyService(configWithPassword);

      expect(configWithPassword.get).toHaveBeenCalledWith("orchestrator.valkey.password");
    });
  });

  describe("Security Warnings (SEC-ORCH-15)", () => {
    it("should check NODE_ENV when VALKEY_PASSWORD not set in production", () => {
      const configNoPassword = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          const config: Record<string, unknown> = {
            "orchestrator.valkey.host": "localhost",
            "orchestrator.valkey.port": 6379,
            NODE_ENV: "production",
          };
          return config[key] ?? defaultValue;
        }),
      } as unknown as ConfigService;

      // Create a service to trigger the warning
      const testService = new ValkeyService(configNoPassword);
      expect(testService).toBeDefined();

      // Verify NODE_ENV was checked (warning path was taken)
      expect(configNoPassword.get).toHaveBeenCalledWith("NODE_ENV", "development");
    });

    it("should check NODE_ENV when VALKEY_PASSWORD not set in development", () => {
      const configNoPasswordDev = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          const config: Record<string, unknown> = {
            "orchestrator.valkey.host": "localhost",
            "orchestrator.valkey.port": 6379,
            NODE_ENV: "development",
          };
          return config[key] ?? defaultValue;
        }),
      } as unknown as ConfigService;

      const testService = new ValkeyService(configNoPasswordDev);
      expect(testService).toBeDefined();

      // Verify NODE_ENV was checked (warning path was taken)
      expect(configNoPasswordDev.get).toHaveBeenCalledWith("NODE_ENV", "development");
    });

    it("should not check NODE_ENV when VALKEY_PASSWORD is configured", () => {
      const configWithPassword = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          const config: Record<string, unknown> = {
            "orchestrator.valkey.host": "localhost",
            "orchestrator.valkey.port": 6379,
            "orchestrator.valkey.password": "secure-password",
            NODE_ENV: "production",
          };
          return config[key] ?? defaultValue;
        }),
      } as unknown as ConfigService;

      const testService = new ValkeyService(configWithPassword);
      expect(testService).toBeDefined();

      // NODE_ENV should NOT be checked when password is set (warning path not taken)
      expect(configWithPassword.get).not.toHaveBeenCalledWith("NODE_ENV", "development");
    });

    it("should default to development environment when NODE_ENV not set", () => {
      const configNoEnv = {
        get: vi.fn((key: string, defaultValue?: unknown) => {
          const config: Record<string, unknown> = {
            "orchestrator.valkey.host": "localhost",
            "orchestrator.valkey.port": 6379,
          };
          // Return default value for NODE_ENV (simulating undefined env var)
          if (key === "NODE_ENV") {
            return defaultValue;
          }
          return config[key] ?? defaultValue;
        }),
      } as unknown as ConfigService;

      const testService = new ValkeyService(configNoEnv);
      expect(testService).toBeDefined();

      // Should have checked NODE_ENV with default "development"
      expect(configNoEnv.get).toHaveBeenCalledWith("NODE_ENV", "development");
    });
  });

  describe("Lifecycle", () => {
    it("should disconnect on module destroy", async () => {
      mockClient.disconnect.mockResolvedValue(undefined);

      await service.onModuleDestroy();

      expect(mockClient.disconnect).toHaveBeenCalled();
    });
  });

  describe("Task State Management", () => {
    const mockTaskState: TaskState = {
      taskId: "task-123",
      status: "pending",
      context: {
        repository: "https://github.com/example/repo",
        branch: "main",
        workItems: ["item-1"],
      },
      createdAt: "2026-02-02T10:00:00Z",
      updatedAt: "2026-02-02T10:00:00Z",
    };

    it("should get task state", async () => {
      mockClient.getTaskState.mockResolvedValue(mockTaskState);

      const result = await service.getTaskState("task-123");

      expect(mockClient.getTaskState).toHaveBeenCalledWith("task-123");
      expect(result).toEqual(mockTaskState);
    });

    it("should set task state", async () => {
      mockClient.setTaskState.mockResolvedValue(undefined);

      await service.setTaskState(mockTaskState);

      expect(mockClient.setTaskState).toHaveBeenCalledWith(mockTaskState);
    });

    it("should delete task state", async () => {
      mockClient.deleteTaskState.mockResolvedValue(undefined);

      await service.deleteTaskState("task-123");

      expect(mockClient.deleteTaskState).toHaveBeenCalledWith("task-123");
    });

    it("should update task status", async () => {
      const updatedTask = { ...mockTaskState, status: "assigned" as const };
      mockClient.updateTaskStatus.mockResolvedValue(updatedTask);

      const result = await service.updateTaskStatus("task-123", "assigned", "agent-456");

      expect(mockClient.updateTaskStatus).toHaveBeenCalledWith(
        "task-123",
        "assigned",
        "agent-456",
        undefined
      );
      expect(result).toEqual(updatedTask);
    });

    it("should list all tasks", async () => {
      const tasks = [mockTaskState];
      mockClient.listTasks.mockResolvedValue(tasks);

      const result = await service.listTasks();

      expect(mockClient.listTasks).toHaveBeenCalled();
      expect(result).toEqual(tasks);
    });
  });

  describe("Agent State Management", () => {
    const mockAgentState: AgentState = {
      agentId: "agent-456",
      status: "spawning",
      taskId: "task-123",
    };

    it("should get agent state", async () => {
      mockClient.getAgentState.mockResolvedValue(mockAgentState);

      const result = await service.getAgentState("agent-456");

      expect(mockClient.getAgentState).toHaveBeenCalledWith("agent-456");
      expect(result).toEqual(mockAgentState);
    });

    it("should set agent state", async () => {
      mockClient.setAgentState.mockResolvedValue(undefined);

      await service.setAgentState(mockAgentState);

      expect(mockClient.setAgentState).toHaveBeenCalledWith(mockAgentState);
    });

    it("should delete agent state", async () => {
      mockClient.deleteAgentState.mockResolvedValue(undefined);

      await service.deleteAgentState("agent-456");

      expect(mockClient.deleteAgentState).toHaveBeenCalledWith("agent-456");
    });

    it("should update agent status", async () => {
      const updatedAgent = { ...mockAgentState, status: "running" as const };
      mockClient.updateAgentStatus.mockResolvedValue(updatedAgent);

      const result = await service.updateAgentStatus("agent-456", "running");

      expect(mockClient.updateAgentStatus).toHaveBeenCalledWith("agent-456", "running", undefined);
      expect(result).toEqual(updatedAgent);
    });

    it("should list all agents", async () => {
      const agents = [mockAgentState];
      mockClient.listAgents.mockResolvedValue(agents);

      const result = await service.listAgents();

      expect(mockClient.listAgents).toHaveBeenCalled();
      expect(result).toEqual(agents);
    });
  });

  describe("Event Pub/Sub", () => {
    const mockEvent: OrchestratorEvent = {
      type: "agent.spawned",
      agentId: "agent-456",
      taskId: "task-123",
      timestamp: "2026-02-02T10:00:00Z",
    };

    it("should publish events", async () => {
      mockClient.publishEvent.mockResolvedValue(undefined);

      await service.publishEvent(mockEvent);

      expect(mockClient.publishEvent).toHaveBeenCalledWith(mockEvent);
    });

    it("should subscribe to events", async () => {
      mockClient.subscribeToEvents.mockResolvedValue(undefined);

      const handler = vi.fn();
      await service.subscribeToEvents(handler);

      expect(mockClient.subscribeToEvents).toHaveBeenCalledWith(handler, undefined);
    });

    it("should subscribe to events with error handler", async () => {
      mockClient.subscribeToEvents.mockResolvedValue(undefined);

      const handler = vi.fn();
      const errorHandler = vi.fn();
      await service.subscribeToEvents(handler, errorHandler);

      expect(mockClient.subscribeToEvents).toHaveBeenCalledWith(handler, errorHandler);
    });
  });

  describe("Convenience Methods", () => {
    it("should create task state with timestamps", async () => {
      mockClient.setTaskState.mockResolvedValue(undefined);

      const context = {
        repository: "https://github.com/example/repo",
        branch: "main",
        workItems: ["item-1"],
      };

      await service.createTask("task-123", context);

      expect(mockClient.setTaskState).toHaveBeenCalledWith({
        taskId: "task-123",
        status: "pending",
        context,
        createdAt: expect.any(String),
        updatedAt: expect.any(String),
      });
    });

    it("should create agent state", async () => {
      mockClient.setAgentState.mockResolvedValue(undefined);

      await service.createAgent("agent-456", "task-123");

      expect(mockClient.setAgentState).toHaveBeenCalledWith({
        agentId: "agent-456",
        status: "spawning",
        taskId: "task-123",
      });
    });
  });
});
