/**
 * EventService Tests
 *
 * Tests for federated event message handling.
 */

import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { EventService } from "./event.service";
import { PrismaService } from "../prisma/prisma.service";
import { FederationService } from "./federation.service";
import { SignatureService } from "./signature.service";
import { HttpService } from "@nestjs/axios";
import { of, throwError } from "rxjs";
import {
  FederationConnectionStatus,
  FederationMessageType,
  FederationMessageStatus,
} from "@prisma/client";
import type { EventMessage, EventAck } from "./types/message.types";
import type { AxiosResponse } from "axios";

describe("EventService", () => {
  let service: EventService;
  let prisma: PrismaService;
  let federationService: FederationService;
  let signatureService: SignatureService;
  let httpService: HttpService;

  const mockWorkspaceId = "workspace-123";
  const mockConnectionId = "connection-123";
  const mockInstanceId = "instance-123";
  const mockRemoteInstanceId = "remote-instance-123";
  const mockMessageId = "message-123";
  const mockEventType = "task.created";

  const mockPrisma = {
    federationConnection: {
      findUnique: vi.fn(),
      findFirst: vi.fn(),
    },
    federationEventSubscription: {
      create: vi.fn(),
      findMany: vi.fn(),
      findUnique: vi.fn(),
      findFirst: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
    },
    federationMessage: {
      create: vi.fn(),
      findMany: vi.fn(),
      findUnique: vi.fn(),
      findFirst: vi.fn(),
      update: vi.fn(),
    },
  };

  const mockFederationService = {
    getInstanceIdentity: vi.fn(),
  };

  const mockSignatureService = {
    signMessage: vi.fn(),
    verifyMessage: vi.fn(),
    validateTimestamp: vi.fn(),
  };

  const mockHttpService = {
    post: vi.fn(),
  };

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

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        EventService,
        {
          provide: PrismaService,
          useValue: mockPrisma,
        },
        {
          provide: FederationService,
          useValue: mockFederationService,
        },
        {
          provide: SignatureService,
          useValue: mockSignatureService,
        },
        {
          provide: HttpService,
          useValue: mockHttpService,
        },
      ],
    }).compile();

    service = module.get<EventService>(EventService);
    prisma = module.get(PrismaService);
    federationService = module.get(FederationService);
    signatureService = module.get(SignatureService);
    httpService = module.get(HttpService);
  });

  describe("subscribeToEventType", () => {
    it("should create a new subscription", async () => {
      const mockConnection = {
        id: mockConnectionId,
        workspaceId: mockWorkspaceId,
        remoteInstanceId: mockRemoteInstanceId,
        remoteUrl: "https://remote.example.com",
        remotePublicKey: "public-key",
        remoteCapabilities: {},
        status: FederationConnectionStatus.ACTIVE,
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
        connectedAt: new Date(),
        disconnectedAt: null,
      };

      const mockSubscription = {
        id: "subscription-123",
        workspaceId: mockWorkspaceId,
        connectionId: mockConnectionId,
        eventType: mockEventType,
        metadata: {},
        isActive: true,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      prisma.federationConnection.findUnique.mockResolvedValue(mockConnection);
      prisma.federationEventSubscription.create.mockResolvedValue(mockSubscription);

      const result = await service.subscribeToEventType(
        mockWorkspaceId,
        mockConnectionId,
        mockEventType
      );

      expect(result).toEqual({
        id: mockSubscription.id,
        workspaceId: mockSubscription.workspaceId,
        connectionId: mockSubscription.connectionId,
        eventType: mockSubscription.eventType,
        metadata: mockSubscription.metadata,
        isActive: mockSubscription.isActive,
        createdAt: mockSubscription.createdAt,
        updatedAt: mockSubscription.updatedAt,
      });

      expect(prisma.federationConnection.findUnique).toHaveBeenCalledWith({
        where: { id: mockConnectionId, workspaceId: mockWorkspaceId },
      });

      expect(prisma.federationEventSubscription.create).toHaveBeenCalledWith({
        data: {
          workspaceId: mockWorkspaceId,
          connectionId: mockConnectionId,
          eventType: mockEventType,
          metadata: {},
        },
      });
    });

    it("should throw error if connection not found", async () => {
      prisma.federationConnection.findUnique.mockResolvedValue(null);

      await expect(
        service.subscribeToEventType(mockWorkspaceId, mockConnectionId, mockEventType)
      ).rejects.toThrow("Connection not found");
    });

    it("should throw error if connection not active", async () => {
      const mockConnection = {
        id: mockConnectionId,
        workspaceId: mockWorkspaceId,
        remoteInstanceId: mockRemoteInstanceId,
        remoteUrl: "https://remote.example.com",
        remotePublicKey: "public-key",
        remoteCapabilities: {},
        status: FederationConnectionStatus.SUSPENDED,
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
        connectedAt: new Date(),
        disconnectedAt: null,
      };

      prisma.federationConnection.findUnique.mockResolvedValue(mockConnection);

      await expect(
        service.subscribeToEventType(mockWorkspaceId, mockConnectionId, mockEventType)
      ).rejects.toThrow("Connection is not active");
    });
  });

  describe("unsubscribeFromEventType", () => {
    it("should delete an existing subscription", async () => {
      const mockSubscription = {
        id: "subscription-123",
        workspaceId: mockWorkspaceId,
        connectionId: mockConnectionId,
        eventType: mockEventType,
        metadata: {},
        isActive: true,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      prisma.federationEventSubscription.findFirst.mockResolvedValue(mockSubscription);
      prisma.federationEventSubscription.delete.mockResolvedValue(mockSubscription);

      await service.unsubscribeFromEventType(mockWorkspaceId, mockConnectionId, mockEventType);

      expect(prisma.federationEventSubscription.findFirst).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          connectionId: mockConnectionId,
          eventType: mockEventType,
        },
      });

      expect(prisma.federationEventSubscription.delete).toHaveBeenCalledWith({
        where: { id: mockSubscription.id },
      });
    });

    it("should throw error if subscription not found", async () => {
      prisma.federationEventSubscription.findFirst.mockResolvedValue(null);

      await expect(
        service.unsubscribeFromEventType(mockWorkspaceId, mockConnectionId, mockEventType)
      ).rejects.toThrow("Subscription not found");
    });
  });

  describe("publishEvent", () => {
    it("should publish event to subscribed connections", async () => {
      const mockConnection = {
        id: mockConnectionId,
        workspaceId: mockWorkspaceId,
        remoteInstanceId: mockRemoteInstanceId,
        remoteUrl: "https://remote.example.com",
        remotePublicKey: "public-key",
        remoteCapabilities: {},
        status: FederationConnectionStatus.ACTIVE,
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
        connectedAt: new Date(),
        disconnectedAt: null,
      };

      const mockSubscription = {
        id: "subscription-123",
        workspaceId: mockWorkspaceId,
        connectionId: mockConnectionId,
        eventType: mockEventType,
        metadata: {},
        isActive: true,
        createdAt: new Date(),
        updatedAt: new Date(),
        connection: mockConnection,
      };

      const mockIdentity = {
        id: "id-123",
        instanceId: mockInstanceId,
        name: "Local Instance",
        url: "https://local.example.com",
        publicKey: "public-key",
        privateKey: "private-key",
        capabilities: {},
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      const mockMessage = {
        id: "message-123",
        workspaceId: mockWorkspaceId,
        connectionId: mockConnectionId,
        messageType: FederationMessageType.EVENT,
        messageId: expect.any(String),
        correlationId: null,
        query: null,
        commandType: null,
        eventType: mockEventType,
        payload: { data: "test" },
        response: null,
        status: FederationMessageStatus.PENDING,
        error: null,
        signature: "signature-123",
        createdAt: new Date(),
        updatedAt: new Date(),
        deliveredAt: null,
      };

      prisma.federationEventSubscription.findMany.mockResolvedValue([mockSubscription]);
      federationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
      signatureService.signMessage.mockResolvedValue("signature-123");
      prisma.federationMessage.create.mockResolvedValue(mockMessage);
      httpService.post.mockReturnValue(
        of({ data: {}, status: 200, statusText: "OK", headers: {}, config: {} as never })
      );

      const result = await service.publishEvent(mockWorkspaceId, mockEventType, { data: "test" });

      expect(result).toHaveLength(1);
      expect(result[0]).toMatchObject({
        id: mockMessage.id,
        workspaceId: mockMessage.workspaceId,
        connectionId: mockMessage.connectionId,
        messageType: mockMessage.messageType,
        eventType: mockMessage.eventType,
        status: mockMessage.status,
      });

      expect(prisma.federationEventSubscription.findMany).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          eventType: mockEventType,
          isActive: true,
        },
        include: {
          connection: true,
        },
      });

      expect(httpService.post).toHaveBeenCalledWith(
        `${mockConnection.remoteUrl}/api/v1/federation/incoming/event`,
        expect.objectContaining({
          instanceId: mockInstanceId,
          eventType: mockEventType,
          payload: { data: "test" },
          signature: "signature-123",
        })
      );
    });

    it("should return empty array if no active subscriptions", async () => {
      prisma.federationEventSubscription.findMany.mockResolvedValue([]);

      const result = await service.publishEvent(mockWorkspaceId, mockEventType, { data: "test" });

      expect(result).toEqual([]);
    });

    it("should handle failed delivery", async () => {
      const mockConnection = {
        id: mockConnectionId,
        workspaceId: mockWorkspaceId,
        remoteInstanceId: mockRemoteInstanceId,
        remoteUrl: "https://remote.example.com",
        remotePublicKey: "public-key",
        remoteCapabilities: {},
        status: FederationConnectionStatus.ACTIVE,
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
        connectedAt: new Date(),
        disconnectedAt: null,
      };

      const mockSubscription = {
        id: "subscription-123",
        workspaceId: mockWorkspaceId,
        connectionId: mockConnectionId,
        eventType: mockEventType,
        metadata: {},
        isActive: true,
        createdAt: new Date(),
        updatedAt: new Date(),
        connection: mockConnection,
      };

      const mockIdentity = {
        id: "id-123",
        instanceId: mockInstanceId,
        name: "Local Instance",
        url: "https://local.example.com",
        publicKey: "public-key",
        privateKey: "private-key",
        capabilities: {},
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      const mockMessage = {
        id: "message-123",
        workspaceId: mockWorkspaceId,
        connectionId: mockConnectionId,
        messageType: FederationMessageType.EVENT,
        messageId: expect.any(String),
        correlationId: null,
        query: null,
        commandType: null,
        eventType: mockEventType,
        payload: { data: "test" },
        response: null,
        status: FederationMessageStatus.PENDING,
        error: null,
        signature: "signature-123",
        createdAt: new Date(),
        updatedAt: new Date(),
        deliveredAt: null,
      };

      prisma.federationEventSubscription.findMany.mockResolvedValue([mockSubscription]);
      federationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
      signatureService.signMessage.mockResolvedValue("signature-123");
      prisma.federationMessage.create.mockResolvedValue(mockMessage);
      httpService.post.mockReturnValue(throwError(() => new Error("Network error")));
      prisma.federationMessage.update.mockResolvedValue({
        ...mockMessage,
        status: FederationMessageStatus.FAILED,
        error: "Network error",
      });

      const result = await service.publishEvent(mockWorkspaceId, mockEventType, { data: "test" });

      expect(result).toHaveLength(1);
      expect(prisma.federationMessage.update).toHaveBeenCalledWith({
        where: { id: mockMessage.id },
        data: {
          status: FederationMessageStatus.FAILED,
          error: "Network error",
        },
      });
    });
  });

  describe("handleIncomingEvent", () => {
    it("should handle incoming event and return acknowledgment", async () => {
      const eventMessage: EventMessage = {
        messageId: mockMessageId,
        instanceId: mockRemoteInstanceId,
        eventType: mockEventType,
        payload: { data: "test" },
        timestamp: Date.now(),
        signature: "signature-123",
      };

      const mockConnection = {
        id: mockConnectionId,
        workspaceId: mockWorkspaceId,
        remoteInstanceId: mockRemoteInstanceId,
        remoteUrl: "https://remote.example.com",
        remotePublicKey: "public-key",
        remoteCapabilities: {},
        status: FederationConnectionStatus.ACTIVE,
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
        connectedAt: new Date(),
        disconnectedAt: null,
      };

      const mockIdentity = {
        id: "id-123",
        instanceId: mockInstanceId,
        name: "Local Instance",
        url: "https://local.example.com",
        publicKey: "public-key",
        privateKey: "private-key",
        capabilities: {},
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      signatureService.validateTimestamp.mockReturnValue(true);
      prisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
      signatureService.verifyMessage.mockResolvedValue({ valid: true, error: null });
      federationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
      signatureService.signMessage.mockResolvedValue("ack-signature-123");

      const result = await service.handleIncomingEvent(eventMessage);

      expect(result).toEqual({
        messageId: expect.any(String),
        correlationId: mockMessageId,
        instanceId: mockInstanceId,
        received: true,
        timestamp: expect.any(Number),
        signature: "ack-signature-123",
      });

      expect(signatureService.validateTimestamp).toHaveBeenCalledWith(eventMessage.timestamp);
      expect(prisma.federationConnection.findFirst).toHaveBeenCalledWith({
        where: {
          remoteInstanceId: mockRemoteInstanceId,
          status: FederationConnectionStatus.ACTIVE,
        },
      });
      expect(signatureService.verifyMessage).toHaveBeenCalledWith(
        {
          messageId: eventMessage.messageId,
          instanceId: eventMessage.instanceId,
          eventType: eventMessage.eventType,
          payload: eventMessage.payload,
          timestamp: eventMessage.timestamp,
        },
        eventMessage.signature,
        eventMessage.instanceId
      );
    });

    it("should throw error for invalid timestamp", async () => {
      const eventMessage: EventMessage = {
        messageId: mockMessageId,
        instanceId: mockRemoteInstanceId,
        eventType: mockEventType,
        payload: { data: "test" },
        timestamp: Date.now(),
        signature: "signature-123",
      };

      signatureService.validateTimestamp.mockReturnValue(false);

      await expect(service.handleIncomingEvent(eventMessage)).rejects.toThrow(
        "Event timestamp is outside acceptable range"
      );
    });

    it("should throw error if no active connection found", async () => {
      const eventMessage: EventMessage = {
        messageId: mockMessageId,
        instanceId: mockRemoteInstanceId,
        eventType: mockEventType,
        payload: { data: "test" },
        timestamp: Date.now(),
        signature: "signature-123",
      };

      signatureService.validateTimestamp.mockReturnValue(true);
      prisma.federationConnection.findFirst.mockResolvedValue(null);

      await expect(service.handleIncomingEvent(eventMessage)).rejects.toThrow(
        "No connection found for remote instance"
      );
    });

    it("should throw error for invalid signature", async () => {
      const eventMessage: EventMessage = {
        messageId: mockMessageId,
        instanceId: mockRemoteInstanceId,
        eventType: mockEventType,
        payload: { data: "test" },
        timestamp: Date.now(),
        signature: "signature-123",
      };

      const mockConnection = {
        id: mockConnectionId,
        workspaceId: mockWorkspaceId,
        remoteInstanceId: mockRemoteInstanceId,
        remoteUrl: "https://remote.example.com",
        remotePublicKey: "public-key",
        remoteCapabilities: {},
        status: FederationConnectionStatus.ACTIVE,
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
        connectedAt: new Date(),
        disconnectedAt: null,
      };

      signatureService.validateTimestamp.mockReturnValue(true);
      prisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
      signatureService.verifyMessage.mockResolvedValue({
        valid: false,
        error: "Invalid signature",
      });

      await expect(service.handleIncomingEvent(eventMessage)).rejects.toThrow("Invalid signature");
    });
  });

  describe("processEventAck", () => {
    it("should process event acknowledgment", async () => {
      const ack: EventAck = {
        messageId: "ack-123",
        correlationId: mockMessageId,
        instanceId: mockRemoteInstanceId,
        received: true,
        timestamp: Date.now(),
        signature: "ack-signature-123",
      };

      const mockMessage = {
        id: "message-123",
        workspaceId: mockWorkspaceId,
        connectionId: mockConnectionId,
        messageType: FederationMessageType.EVENT,
        messageId: mockMessageId,
        correlationId: null,
        query: null,
        commandType: null,
        eventType: mockEventType,
        payload: { data: "test" },
        response: null,
        status: FederationMessageStatus.PENDING,
        error: null,
        signature: "signature-123",
        createdAt: new Date(),
        updatedAt: new Date(),
        deliveredAt: null,
      };

      signatureService.validateTimestamp.mockReturnValue(true);
      prisma.federationMessage.findFirst.mockResolvedValue(mockMessage);
      signatureService.verifyMessage.mockResolvedValue({ valid: true, error: null });
      prisma.federationMessage.update.mockResolvedValue({
        ...mockMessage,
        status: FederationMessageStatus.DELIVERED,
        deliveredAt: new Date(),
      });

      await service.processEventAck(ack);

      expect(signatureService.validateTimestamp).toHaveBeenCalledWith(ack.timestamp);
      expect(prisma.federationMessage.findFirst).toHaveBeenCalledWith({
        where: {
          messageId: ack.correlationId,
          messageType: FederationMessageType.EVENT,
        },
      });
      expect(prisma.federationMessage.update).toHaveBeenCalledWith({
        where: { id: mockMessage.id },
        data: {
          status: FederationMessageStatus.DELIVERED,
          deliveredAt: expect.any(Date),
        },
      });
    });

    it("should throw error if original event not found", async () => {
      const ack: EventAck = {
        messageId: "ack-123",
        correlationId: mockMessageId,
        instanceId: mockRemoteInstanceId,
        received: true,
        timestamp: Date.now(),
        signature: "ack-signature-123",
      };

      signatureService.validateTimestamp.mockReturnValue(true);
      prisma.federationMessage.findFirst.mockResolvedValue(null);

      await expect(service.processEventAck(ack)).rejects.toThrow(
        "Original event message not found"
      );
    });
  });

  describe("getEventSubscriptions", () => {
    it("should return all subscriptions for workspace", async () => {
      const mockSubscriptions = [
        {
          id: "sub-1",
          workspaceId: mockWorkspaceId,
          connectionId: mockConnectionId,
          eventType: "task.created",
          metadata: {},
          isActive: true,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
        {
          id: "sub-2",
          workspaceId: mockWorkspaceId,
          connectionId: mockConnectionId,
          eventType: "task.updated",
          metadata: {},
          isActive: true,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ];

      prisma.federationEventSubscription.findMany.mockResolvedValue(mockSubscriptions);

      const result = await service.getEventSubscriptions(mockWorkspaceId);

      expect(result).toHaveLength(2);
      expect(prisma.federationEventSubscription.findMany).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
        },
        orderBy: { createdAt: "desc" },
      });
    });

    it("should filter by connectionId when provided", async () => {
      const mockSubscriptions = [
        {
          id: "sub-1",
          workspaceId: mockWorkspaceId,
          connectionId: mockConnectionId,
          eventType: "task.created",
          metadata: {},
          isActive: true,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ];

      prisma.federationEventSubscription.findMany.mockResolvedValue(mockSubscriptions);

      const result = await service.getEventSubscriptions(mockWorkspaceId, mockConnectionId);

      expect(result).toHaveLength(1);
      expect(prisma.federationEventSubscription.findMany).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          connectionId: mockConnectionId,
        },
        orderBy: { createdAt: "desc" },
      });
    });
  });

  describe("getEventMessages", () => {
    it("should return all event messages for workspace", async () => {
      const mockMessages = [
        {
          id: "msg-1",
          workspaceId: mockWorkspaceId,
          connectionId: mockConnectionId,
          messageType: FederationMessageType.EVENT,
          messageId: "msg-id-1",
          correlationId: null,
          query: null,
          commandType: null,
          eventType: "task.created",
          payload: { data: "test1" },
          response: null,
          status: FederationMessageStatus.DELIVERED,
          error: null,
          signature: "sig-1",
          createdAt: new Date(),
          updatedAt: new Date(),
          deliveredAt: new Date(),
        },
        {
          id: "msg-2",
          workspaceId: mockWorkspaceId,
          connectionId: mockConnectionId,
          messageType: FederationMessageType.EVENT,
          messageId: "msg-id-2",
          correlationId: null,
          query: null,
          commandType: null,
          eventType: "task.updated",
          payload: { data: "test2" },
          response: null,
          status: FederationMessageStatus.PENDING,
          error: null,
          signature: "sig-2",
          createdAt: new Date(),
          updatedAt: new Date(),
          deliveredAt: null,
        },
      ];

      prisma.federationMessage.findMany.mockResolvedValue(mockMessages);

      const result = await service.getEventMessages(mockWorkspaceId);

      expect(result).toHaveLength(2);
      expect(prisma.federationMessage.findMany).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          messageType: FederationMessageType.EVENT,
        },
        orderBy: { createdAt: "desc" },
      });
    });

    it("should filter by status when provided", async () => {
      const mockMessages = [
        {
          id: "msg-1",
          workspaceId: mockWorkspaceId,
          connectionId: mockConnectionId,
          messageType: FederationMessageType.EVENT,
          messageId: "msg-id-1",
          correlationId: null,
          query: null,
          commandType: null,
          eventType: "task.created",
          payload: { data: "test1" },
          response: null,
          status: FederationMessageStatus.PENDING,
          error: null,
          signature: "sig-1",
          createdAt: new Date(),
          updatedAt: new Date(),
          deliveredAt: null,
        },
      ];

      prisma.federationMessage.findMany.mockResolvedValue(mockMessages);

      const result = await service.getEventMessages(
        mockWorkspaceId,
        FederationMessageStatus.PENDING
      );

      expect(result).toHaveLength(1);
      expect(prisma.federationMessage.findMany).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          messageType: FederationMessageType.EVENT,
          status: FederationMessageStatus.PENDING,
        },
        orderBy: { createdAt: "desc" },
      });
    });
  });
});
