import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ConfigService } from "@nestjs/config";
import { Logger, NotFoundException } from "@nestjs/common";
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execFile } from "node:child_process";
import { QueueNotificationsService } from "./queue-notifications.service";

vi.mock("node:child_process", () => ({
  execFile: vi.fn(),
}));

describe("QueueNotificationsService", () => {
  let service: QueueNotificationsService;
  let inboxDir: string;
  let agentStateDir: string;
  let configService: ConfigService;

  beforeEach(async () => {
    vi.clearAllMocks();
    inboxDir = await mkdtemp(join(tmpdir(), "queue-notifications-"));
    agentStateDir = await mkdtemp(join(tmpdir(), "agent-state-"));
    configService = {
      get: vi.fn((key: string) => {
        if (key === "MOSAIC_QUEUE_INBOX_DIR") {
          return inboxDir;
        }

        if (key === "MOSAIC_AGENT_STATE_DIR") {
          return agentStateDir;
        }

        if (key === "MOSAIC_QUEUE_CLI") {
          return "/tmp/mosaic-queue-cli.js";
        }

        return undefined;
      }),
    } as unknown as ConfigService;

    service = new QueueNotificationsService(configService);
  });

  afterEach(async () => {
    vi.restoreAllMocks();
    await rm(inboxDir, { recursive: true, force: true });
    await rm(agentStateDir, { recursive: true, force: true });
  });

  describe("onModuleInit", () => {
    it("logs a warning when the inbox directory does not exist", async () => {
      await rm(inboxDir, { recursive: true, force: true });
      const warnSpy = vi.spyOn(Logger.prototype, "warn").mockImplementation(() => undefined);

      await service.onModuleInit();

      expect(warnSpy).toHaveBeenCalledWith(
        expect.stringContaining("Queue notifications inbox directory does not exist")
      );
    });
  });

  describe("listNotifications", () => {
    it("returns parsed notifications from agent inbox directories", async () => {
      await mkdir(join(inboxDir, "mosaic"), { recursive: true });
      await mkdir(join(inboxDir, "mosaic", "_acked"), { recursive: true });
      await mkdir(join(inboxDir, "sage"), { recursive: true });
      await writeFile(
        join(inboxDir, "mosaic", "notif-1.json"),
        JSON.stringify({ type: "task.ready", taskId: "MS24-API-001" })
      );
      await writeFile(
        join(inboxDir, "mosaic", "_acked", "notif-ignored.json"),
        JSON.stringify({ ignored: true })
      );
      await writeFile(join(inboxDir, "sage", "notif-2.json"), JSON.stringify({ type: "done" }));

      const notifications = await service.listNotifications();

      expect(notifications).toHaveLength(2);
      expect(notifications).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            id: "notif-1",
            agent: "mosaic",
            filename: "notif-1.json",
            payload: { type: "task.ready", taskId: "MS24-API-001" },
          }),
          expect.objectContaining({
            id: "notif-2",
            agent: "sage",
            filename: "notif-2.json",
            payload: { type: "done" },
          }),
        ])
      );
    });

    it("returns an empty array when the inbox directory is missing", async () => {
      await rm(inboxDir, { recursive: true, force: true });

      await expect(service.listNotifications()).resolves.toEqual([]);
    });
  });

  describe("ackNotification", () => {
    it("executes the queue CLI with node and ack args", async () => {
      await mkdir(join(inboxDir, "mosaic"), { recursive: true });
      await writeFile(join(inboxDir, "mosaic", "notif-3.json"), JSON.stringify({ ok: true }));
      vi.mocked(execFile).mockImplementation(
        (
          _command: string,
          _args: readonly string[],
          callback: (error: Error | null, stdout: string, stderr: string) => void
        ) => callback(null, "acked", "")
      );

      await expect(service.ackNotification("notif-3")).resolves.toEqual({
        success: true,
        id: "notif-3",
      });

      expect(execFile).toHaveBeenCalledWith(
        "node",
        ["/tmp/mosaic-queue-cli.js", "ack", "notif-3"],
        expect.any(Function)
      );
    });

    it("throws NotFoundException when the notification does not exist", async () => {
      await expect(service.ackNotification("missing")).rejects.toThrow(NotFoundException);
      expect(execFile).not.toHaveBeenCalled();
    });
  });

  describe("listTasks", () => {
    it("parses tab-separated CLI output", async () => {
      vi.mocked(execFile).mockImplementation(
        (
          _command: string,
          _args: readonly string[],
          callback: (error: Error | null, stdout: string, stderr: string) => void
        ) =>
          callback(
            null,
            [
              "task-1\tmosaic-stack/MS24-API-001\t[pending]\tBuild queue notifications module",
              "task-2\tmosaic-stack/MS24-API-002\t[done]\tWrite tests",
            ].join("\n"),
            ""
          )
      );

      await expect(service.listTasks()).resolves.toEqual([
        {
          id: "task-1",
          project: "mosaic-stack",
          taskId: "MS24-API-001",
          status: "pending",
          description: "Build queue notifications module",
        },
        {
          id: "task-2",
          project: "mosaic-stack",
          taskId: "MS24-API-002",
          status: "done",
          description: "Write tests",
        },
      ]);

      expect(execFile).toHaveBeenCalledWith(
        "node",
        ["/tmp/mosaic-queue-cli.js", "list", "mosaic-stack"],
        expect.any(Function)
      );
    });
  });

  describe("notifyAgentCiResult", () => {
    it("writes one notification per active agent-state record matching the branch", async () => {
      await mkdir(join(agentStateDir, "active"), { recursive: true });
      await writeFile(
        join(agentStateDir, "active", "sage-landing-page.json"),
        JSON.stringify({
          taskId: "sage-landing-page",
          status: "spawned",
          startedAt: "2026-03-08T22:47:20Z",
          lastUpdated: "2026-03-08T23:15:11.726Z",
          agent: "sage",
          branch: "feature/landing-page",
          description: "Create apps/landing marketing site",
        })
      );
      await writeFile(
        join(agentStateDir, "active", "pixels-other.json"),
        JSON.stringify({
          taskId: "pixels-other",
          status: "spawned",
          startedAt: "2026-03-08T22:47:20Z",
          lastUpdated: "2026-03-08T23:15:11.726Z",
          agent: "pixels",
          branch: "feature/something-else",
          description: "Unrelated",
        })
      );

      await expect(
        service.notifyAgentCiResult({
          branch: "feature/landing-page",
          status: "success",
          buildUrl: "https://ci.example/build/123",
          repo: "mosaic/stack",
        })
      ).resolves.toEqual({ notified: 1 });

      const inboxFiles = await readdir(join(inboxDir, "sage"));
      expect(inboxFiles).toHaveLength(1);

      const notification = JSON.parse(
        await readFile(join(inboxDir, "sage", inboxFiles[0]!), "utf8")
      ) as Record<string, unknown>;

      expect(notification).toMatchObject({
        taskId: "sage-landing-page",
        event: "completed",
        targetAgent: "sage",
        fromAgent: "mosaic-api",
        retries: 0,
        maxRetries: 3,
        ttlSeconds: 600,
        payload: {
          branch: "feature/landing-page",
          buildUrl: "https://ci.example/build/123",
          repo: "mosaic/stack",
          ciStatus: "success",
        },
      });
      expect(typeof notification.id).toBe("string");
      expect(typeof notification.createdAt).toBe("string");
    });

    it("returns zero when no active agent-state branch matches the webhook payload", async () => {
      await mkdir(join(agentStateDir, "active"), { recursive: true });
      await writeFile(
        join(agentStateDir, "active", "mosaic-task.json"),
        JSON.stringify({
          taskId: "mosaic-task",
          status: "spawned",
          startedAt: "2026-03-08T22:47:20Z",
          lastUpdated: "2026-03-08T23:15:11.726Z",
          agent: "mosaic",
          branch: "feature/not-this-one",
          description: "Unrelated",
        })
      );

      await expect(
        service.notifyAgentCiResult({
          branch: "feature/landing-page",
          status: "failure",
          buildUrl: "https://ci.example/build/456",
          repo: "mosaic/stack",
        })
      ).resolves.toEqual({ notified: 0 });
    });
  });
});
