import { describe, it, expect, vi, beforeEach } from "vitest";
import { TextbooksService } from "./textbooks.service.js";
import { NotFoundError } from "../shared/errors/application-error.js";

vi.mock("./textbooks.repository.js", () => ({
  textbooksRepository: {
    create: vi.fn(),
    findById: vi.fn(),
    find: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  },
}));

import { textbooksRepository } from "./textbooks.repository.js";

const mockOutbox = {
  publish: vi.fn().mockResolvedValue("event-id"),
};

describe("TextbooksService", () => {
  let service: TextbooksService;

  beforeEach(() => {
    vi.clearAllMocks();
    service = new TextbooksService(mockOutbox as never);
  });

  describe("create", () => {
    it("should create a textbook and publish event", async () => {
      const input = {
        title: "Math Grade 3",
        subjectId: "subj-1",
        gradeId: "grade-3",
      };
      const result = await service.create(input);

      expect(result.id).toBeDefined();
      expect(result.id).toHaveLength(24); // cuid2 length
      expect(textbooksRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          title: "Math Grade 3",
          subjectId: "subj-1",
          gradeId: "grade-3",
          version: "1.0",
          status: "draft",
        }),
      );
      expect(mockOutbox.publish).toHaveBeenCalledWith(
        "textbook.created",
        "Textbook",
        result.id,
        expect.objectContaining({
          title: "Math Grade 3",
          subject_id: "subj-1",
          grade_id: "grade-3",
        }),
      );
    });

    it("should use provided version when specified", async () => {
      const result = await service.create({
        title: "Test",
        subjectId: "s",
        gradeId: "g",
        version: "2.0",
      });
      expect(textbooksRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({ version: "2.0" }),
      );
      expect(result.id).toBeDefined();
    });
  });

  describe("getById", () => {
    it("should return textbook when found", async () => {
      const mockTextbook = {
        id: "kp-1",
        title: "Test",
        subjectId: "s",
        gradeId: "g",
        version: "1.0",
        status: "draft",
        tenantId: null,
        metadata: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };
      vi.mocked(textbooksRepository.findById).mockResolvedValue(mockTextbook);
      const result = await service.getById("kp-1");
      expect(result).toBe(mockTextbook);
    });

    it("should throw NotFoundError when not found", async () => {
      vi.mocked(textbooksRepository.findById).mockResolvedValue(undefined);
      await expect(service.getById("nonexistent")).rejects.toThrow(
        NotFoundError,
      );
    });
  });

  describe("update", () => {
    it("should update and publish updated event", async () => {
      const existing = {
        id: "t-1",
        title: "Old",
        subjectId: "s",
        gradeId: "g",
        version: "1.0",
        status: "draft",
        tenantId: null,
        metadata: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };
      vi.mocked(textbooksRepository.findById).mockResolvedValue(existing);

      await service.update("t-1", { title: "New Title" });

      expect(textbooksRepository.update).toHaveBeenCalledWith("t-1", {
        title: "New Title",
      });
      expect(mockOutbox.publish).toHaveBeenCalledWith(
        "textbook.updated",
        "Textbook",
        "t-1",
        expect.objectContaining({ title: "New Title" }),
      );
    });

    it("should publish published event when status is published", async () => {
      vi.mocked(textbooksRepository.findById).mockResolvedValue({
        id: "t-1",
        title: "T",
        subjectId: "s",
        gradeId: "g",
        version: "1.0",
        status: "draft",
        tenantId: null,
        metadata: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      });

      await service.update("t-1", { status: "published" });

      expect(mockOutbox.publish).toHaveBeenCalledWith(
        "textbook.published",
        "Textbook",
        "t-1",
        expect.any(Object),
      );
    });

    it("should publish archived event when status is archived", async () => {
      vi.mocked(textbooksRepository.findById).mockResolvedValue({
        id: "t-1",
        title: "T",
        subjectId: "s",
        gradeId: "g",
        version: "1.0",
        status: "draft",
        tenantId: null,
        metadata: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      });

      await service.update("t-1", { status: "archived" });

      expect(mockOutbox.publish).toHaveBeenCalledWith(
        "textbook.archived",
        "Textbook",
        "t-1",
        expect.any(Object),
      );
    });
  });

  describe("delete", () => {
    it("should delete and publish archived event", async () => {
      vi.mocked(textbooksRepository.findById).mockResolvedValue({
        id: "t-1",
        title: "T",
        subjectId: "s",
        gradeId: "g",
        version: "1.0",
        status: "draft",
        tenantId: null,
        metadata: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      });

      await service.delete("t-1");

      expect(textbooksRepository.delete).toHaveBeenCalledWith("t-1");
      expect(mockOutbox.publish).toHaveBeenCalledWith(
        "textbook.archived",
        "Textbook",
        "t-1",
        { deleted: true },
      );
    });
  });

  describe("list", () => {
    it("should call repository find with query", async () => {
      const mockList = [{ id: "t-1" }];
      vi.mocked(textbooksRepository.find).mockResolvedValue(mockList as never);
      const result = await service.list({ subjectId: "s" });
      expect(result).toBe(mockList);
      expect(textbooksRepository.find).toHaveBeenCalledWith({ subjectId: "s" });
    });
  });
});
