import { Test, TestingModule } from "@nestjs/testing";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { HeraldService } from "./herald.service";
import { PrismaService } from "../prisma/prisma.service";
import { DiscordService } from "../bridge/discord/discord.service";
import {
  JOB_CREATED,
  JOB_STARTED,
  JOB_COMPLETED,
  JOB_FAILED,
  STEP_STARTED,
  STEP_COMPLETED,
  GATE_PASSED,
  GATE_FAILED,
} from "../job-events/event-types";

describe("HeraldService", () => {
  let service: HeraldService;
  let prisma: PrismaService;
  let discord: DiscordService;

  const mockPrisma = {
    workspace: {
      findUnique: vi.fn(),
    },
    runnerJob: {
      findUnique: vi.fn(),
    },
    jobEvent: {
      findFirst: vi.fn(),
    },
  };

  const mockDiscord = {
    isConnected: vi.fn(),
    sendMessage: vi.fn(),
    sendThreadMessage: vi.fn(),
    createThread: vi.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        HeraldService,
        {
          provide: PrismaService,
          useValue: mockPrisma,
        },
        {
          provide: DiscordService,
          useValue: mockDiscord,
        },
      ],
    }).compile();

    service = module.get<HeraldService>(HeraldService);
    prisma = module.get<PrismaService>(PrismaService);
    discord = module.get<DiscordService>(DiscordService);

    // Reset mocks
    vi.clearAllMocks();
  });

  describe("broadcastJobEvent", () => {
    it("should broadcast job.created event to configured channel", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobId = "job-1";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_CREATED,
        timestamp: new Date(),
        actor: "system",
        payload: { issueNumber: 42 },
      };

      mockPrisma.workspace.findUnique.mockResolvedValue({
        id: workspaceId,
        settings: {
          herald: {
            channelMappings: {
              "code-task": "channel-123",
            },
          },
        },
      });

      mockPrisma.runnerJob.findUnique.mockResolvedValue({
        id: jobId,
        workspaceId,
        type: "code-task",
      });

      mockPrisma.jobEvent.findFirst.mockResolvedValue({
        payload: {
          metadata: { issueNumber: 42, threadId: "thread-123" },
        },
      });

      mockDiscord.isConnected.mockReturnValue(true);
      mockDiscord.sendThreadMessage.mockResolvedValue(undefined);

      // Act
      await service.broadcastJobEvent(jobId, event);

      // Assert
      expect(mockDiscord.sendThreadMessage).toHaveBeenCalledWith({
        threadId: "thread-123",
        content: expect.stringContaining("Job created"),
      });
    });

    it("should broadcast job.started event", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobId = "job-1";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_STARTED,
        timestamp: new Date(),
        actor: "system",
        payload: {},
      };

      mockPrisma.workspace.findUnique.mockResolvedValue({
        id: workspaceId,
        settings: { herald: { channelMappings: {} } },
      });

      mockPrisma.runnerJob.findUnique.mockResolvedValue({
        id: jobId,
        workspaceId,
        type: "code-task",
      });

      mockPrisma.jobEvent.findFirst.mockResolvedValue({
        payload: {
          metadata: { threadId: "thread-123" },
        },
      });

      mockDiscord.isConnected.mockReturnValue(true);
      mockDiscord.sendThreadMessage.mockResolvedValue(undefined);

      // Act
      await service.broadcastJobEvent(jobId, event);

      // Assert
      expect(mockDiscord.sendThreadMessage).toHaveBeenCalledWith({
        threadId: "thread-123",
        content: expect.stringContaining("Job started"),
      });
    });

    it("should broadcast job.completed event with success message", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobId = "job-1";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_COMPLETED,
        timestamp: new Date(),
        actor: "system",
        payload: { duration: 120 },
      };

      mockPrisma.workspace.findUnique.mockResolvedValue({
        id: workspaceId,
        settings: { herald: { channelMappings: {} } },
      });

      mockPrisma.runnerJob.findUnique.mockResolvedValue({
        id: jobId,
        workspaceId,
        type: "code-task",
      });

      mockPrisma.jobEvent.findFirst.mockResolvedValue({
        payload: {
          metadata: { threadId: "thread-123" },
        },
      });

      mockDiscord.isConnected.mockReturnValue(true);
      mockDiscord.sendThreadMessage.mockResolvedValue(undefined);

      // Act
      await service.broadcastJobEvent(jobId, event);

      // Assert
      expect(mockDiscord.sendThreadMessage).toHaveBeenCalledWith({
        threadId: "thread-123",
        content: expect.stringContaining("completed"),
      });
    });

    it("should broadcast job.failed event with PDA-friendly language", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobId = "job-1";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_FAILED,
        timestamp: new Date(),
        actor: "system",
        payload: { error: "Build failed" },
      };

      mockPrisma.workspace.findUnique.mockResolvedValue({
        id: workspaceId,
        settings: { herald: { channelMappings: {} } },
      });

      mockPrisma.runnerJob.findUnique.mockResolvedValue({
        id: jobId,
        workspaceId,
        type: "code-task",
      });

      mockPrisma.jobEvent.findFirst.mockResolvedValue({
        payload: {
          metadata: { threadId: "thread-123" },
        },
      });

      mockDiscord.isConnected.mockReturnValue(true);
      mockDiscord.sendThreadMessage.mockResolvedValue(undefined);

      // Act
      await service.broadcastJobEvent(jobId, event);

      // Assert
      expect(mockDiscord.sendThreadMessage).toHaveBeenCalledWith({
        threadId: "thread-123",
        content: expect.stringContaining("encountered an issue"),
      });
      // Verify the actual message doesn't contain demanding language
      const actualCall = mockDiscord.sendThreadMessage.mock.calls[0][0];
      expect(actualCall.content).not.toMatch(/FAILED|ERROR|CRITICAL|URGENT/);
    });

    it("should skip broadcasting if Discord is not connected", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobId = "job-1";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_CREATED,
        timestamp: new Date(),
        actor: "system",
        payload: {},
      };

      mockPrisma.workspace.findUnique.mockResolvedValue({
        id: workspaceId,
        settings: { herald: { channelMappings: {} } },
      });

      mockPrisma.runnerJob.findUnique.mockResolvedValue({
        id: jobId,
        workspaceId,
        type: "code-task",
      });

      mockPrisma.jobEvent.findFirst.mockResolvedValue({
        payload: {
          metadata: { threadId: "thread-123" },
        },
      });

      mockDiscord.isConnected.mockReturnValue(false);

      // Act
      await service.broadcastJobEvent(jobId, event);

      // Assert
      expect(mockDiscord.sendThreadMessage).not.toHaveBeenCalled();
    });

    it("should skip broadcasting if job has no threadId", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobId = "job-1";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_CREATED,
        timestamp: new Date(),
        actor: "system",
        payload: {},
      };

      mockPrisma.workspace.findUnique.mockResolvedValue({
        id: workspaceId,
        settings: { herald: { channelMappings: {} } },
      });

      mockPrisma.runnerJob.findUnique.mockResolvedValue({
        id: jobId,
        workspaceId,
        type: "code-task",
      });

      mockPrisma.jobEvent.findFirst.mockResolvedValue({
        payload: {
          metadata: {}, // No threadId
        },
      });

      mockDiscord.isConnected.mockReturnValue(true);

      // Act
      await service.broadcastJobEvent(jobId, event);

      // Assert
      expect(mockDiscord.sendThreadMessage).not.toHaveBeenCalled();
    });

    // ERROR HANDLING TESTS - Issue #185

    it("should propagate database errors when job lookup fails", async () => {
      // Arrange
      const jobId = "job-1";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_CREATED,
        timestamp: new Date(),
        actor: "system",
        payload: {},
      };

      const dbError = new Error("Database connection lost");
      mockPrisma.runnerJob.findUnique.mockRejectedValue(dbError);

      // Act & Assert
      await expect(service.broadcastJobEvent(jobId, event)).rejects.toThrow(
        "Database connection lost"
      );
    });

    it("should propagate Discord send failures with context", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobId = "job-1";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_CREATED,
        timestamp: new Date(),
        actor: "system",
        payload: {},
      };

      mockPrisma.runnerJob.findUnique.mockResolvedValue({
        id: jobId,
        workspaceId,
        type: "code-task",
      });

      mockPrisma.jobEvent.findFirst.mockResolvedValue({
        payload: {
          metadata: { threadId: "thread-123" },
        },
      });

      mockDiscord.isConnected.mockReturnValue(true);

      const discordError = new Error("Rate limit exceeded");
      mockDiscord.sendThreadMessage.mockRejectedValue(discordError);

      // Act & Assert
      await expect(service.broadcastJobEvent(jobId, event)).rejects.toThrow("Rate limit exceeded");
    });

    it("should propagate errors when fetching job events fails", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobId = "job-1";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_STARTED,
        timestamp: new Date(),
        actor: "system",
        payload: {},
      };

      mockPrisma.runnerJob.findUnique.mockResolvedValue({
        id: jobId,
        workspaceId,
        type: "code-task",
      });

      const dbError = new Error("Query timeout");
      mockPrisma.jobEvent.findFirst.mockRejectedValue(dbError);

      mockDiscord.isConnected.mockReturnValue(true);

      // Act & Assert
      await expect(service.broadcastJobEvent(jobId, event)).rejects.toThrow("Query timeout");
    });

    it("should include job context in error messages", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobId = "test-job-123";
      const event = {
        id: "event-1",
        jobId,
        type: JOB_COMPLETED,
        timestamp: new Date(),
        actor: "system",
        payload: {},
      };

      mockPrisma.runnerJob.findUnique.mockResolvedValue({
        id: jobId,
        workspaceId,
        type: "code-task",
      });

      mockPrisma.jobEvent.findFirst.mockResolvedValue({
        payload: {
          metadata: { threadId: "thread-123" },
        },
      });

      mockDiscord.isConnected.mockReturnValue(true);

      const discordError = new Error("Network failure");
      mockDiscord.sendThreadMessage.mockRejectedValue(discordError);

      // Act & Assert
      try {
        await service.broadcastJobEvent(jobId, event);
        // Should not reach here
        expect(true).toBe(false);
      } catch (error) {
        // Verify error was thrown
        expect(error).toBeDefined();
        // Verify original error is preserved
        expect((error as Error).message).toContain("Network failure");
      }
    });
  });

  describe("formatJobEventMessage", () => {
    it("should format job.created message with 10-second scannability", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        type: JOB_CREATED,
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: { issueNumber: 42 },
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      const metadata = { issueNumber: 42 };

      // Act
      const message = service.formatJobEventMessage(event, job, metadata);

      // Assert
      expect(message).toContain("🟢");
      expect(message).toContain("Job created");
      expect(message).toContain("#42");
      expect(message.length).toBeLessThan(200); // Keep it scannable
    });

    it("should format job.created without issue number", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        type: JOB_CREATED,
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: {},
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      // Act
      const message = service.formatJobEventMessage(event, job, undefined);

      // Assert
      expect(message).toContain("Job created");
      expect(message).toContain("task");
      expect(message).not.toContain("#");
    });

    it("should format job.completed message with visual indicator", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        type: JOB_COMPLETED,
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: { duration: 120 },
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      const metadata = { issueNumber: 42 };

      // Act
      const message = service.formatJobEventMessage(event, job, metadata);

      // Assert
      expect(message).toMatch(/✅|🟢/);
      expect(message).toContain("completed");
      expect(message).not.toMatch(/COMPLETED|SUCCESS/);
    });

    it("should format step.completed message", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        stepId: "step-1",
        type: STEP_COMPLETED,
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: { stepName: "Run tests" },
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      const metadata = { issueNumber: 42 };

      // Act
      const message = service.formatJobEventMessage(event, job, metadata);

      // Assert
      expect(message).toContain("Step completed");
      expect(message).toContain("Run tests");
    });

    it("should format step.started message", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        stepId: "step-1",
        type: STEP_STARTED,
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: { stepName: "Build project" },
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      // Act
      const message = service.formatJobEventMessage(event, job, {});

      // Assert
      expect(message).toContain("Step started");
      expect(message).toContain("Build project");
    });

    it("should format step.started without step name", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        stepId: "step-1",
        type: STEP_STARTED,
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: {},
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      // Act
      const message = service.formatJobEventMessage(event, job, {});

      // Assert
      expect(message).toContain("Step started");
      expect(message).toContain("unknown");
    });

    it("should format gate.passed message", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        type: GATE_PASSED,
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: { gateName: "build" },
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      const metadata = { issueNumber: 42 };

      // Act
      const message = service.formatJobEventMessage(event, job, metadata);

      // Assert
      expect(message).toContain("Gate passed");
      expect(message).toContain("build");
    });

    it("should format gate.failed message with PDA-friendly language", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        type: GATE_FAILED,
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: { gateName: "test", error: "2 tests failed" },
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      const metadata = { issueNumber: 42 };

      // Act
      const message = service.formatJobEventMessage(event, job, metadata);

      // Assert
      expect(message).toContain("Gate needs attention");
      expect(message).toContain("test");
      expect(message).not.toMatch(/FAILED|ERROR|CRITICAL/);
    });

    it("should format gate.failed without error details", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        type: GATE_FAILED,
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: { gateName: "lint" },
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      // Act
      const message = service.formatJobEventMessage(event, job, {});

      // Assert
      expect(message).toContain("Gate needs attention");
      expect(message).toContain("lint");
      expect(message).not.toContain("\n");
    });

    it("should format step.failed with error message", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        stepId: "step-1",
        type: "step.failed",
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: { stepName: "Deploy", error: "Connection timeout" },
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      // Act
      const message = service.formatJobEventMessage(event, job, {});

      // Assert
      expect(message).toContain("Step needs attention");
      expect(message).toContain("Deploy");
      expect(message).toContain("Connection timeout");
    });

    it("should format job.cancelled message", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        type: "job.cancelled",
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "user",
        payload: {},
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      const metadata = { issueNumber: 123 };

      // Act
      const message = service.formatJobEventMessage(event, job, metadata);

      // Assert
      expect(message).toContain("Job paused");
      expect(message).toContain("#123");
    });

    it("should format unknown event types", () => {
      // Arrange
      const event = {
        id: "event-1",
        jobId: "job-1",
        type: "unknown.event.type",
        timestamp: new Date("2026-01-01T12:00:00Z"),
        actor: "system",
        payload: {},
      };

      const job = {
        id: "job-1",
        type: "code-task",
      };

      // Act
      const message = service.formatJobEventMessage(event, job, {});

      // Assert
      expect(message).toContain("Event: unknown.event.type");
    });
  });

  describe("getChannelForJobType", () => {
    it("should return channel from workspace settings", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobType = "code-task";

      mockPrisma.workspace.findUnique.mockResolvedValue({
        id: workspaceId,
        settings: {
          herald: {
            channelMappings: {
              "code-task": "channel-123",
            },
          },
        },
      });

      // Act
      const channelId = await service.getChannelForJobType(workspaceId, jobType);

      // Assert
      expect(channelId).toBe("channel-123");
    });

    it("should return default channel if job type not mapped", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobType = "code-task";

      mockPrisma.workspace.findUnique.mockResolvedValue({
        id: workspaceId,
        settings: {
          herald: {
            channelMappings: {},
            defaultChannel: "default-channel",
          },
        },
      });

      // Act
      const channelId = await service.getChannelForJobType(workspaceId, jobType);

      // Assert
      expect(channelId).toBe("default-channel");
    });

    it("should return null if no channel configured", async () => {
      // Arrange
      const workspaceId = "workspace-1";
      const jobType = "code-task";

      mockPrisma.workspace.findUnique.mockResolvedValue({
        id: workspaceId,
        settings: {},
      });

      // Act
      const channelId = await service.getChannelForJobType(workspaceId, jobType);

      // Assert
      expect(channelId).toBeNull();
    });
  });
});
