import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { TagsService } from "./tags.service";
import { PrismaService } from "../prisma/prisma.service";
import { NotFoundException, ConflictException, BadRequestException } from "@nestjs/common";
import type { CreateTagDto, UpdateTagDto } from "./dto";

describe("TagsService", () => {
  let service: TagsService;
  let prisma: PrismaService;

  const workspaceId = "workspace-123";
  const userId = "user-123";

  const mockTag = {
    id: "tag-123",
    workspaceId,
    name: "Architecture",
    slug: "architecture",
    color: "#FF5733",
    description: "Architecture related topics",
  };

  const mockPrismaService = {
    knowledgeTag: {
      create: vi.fn(),
      findMany: vi.fn(),
      findUnique: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
    },
    knowledgeEntry: {
      findMany: vi.fn(),
    },
  };

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

    service = module.get<TagsService>(TagsService);
    prisma = module.get<PrismaService>(PrismaService);

    vi.clearAllMocks();
  });

  describe("create", () => {
    it("should create a tag with auto-generated slug", async () => {
      const createDto: CreateTagDto = {
        name: "Architecture",
        color: "#FF5733",
        description: "Architecture related topics",
      };

      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue(null);
      mockPrismaService.knowledgeTag.create.mockResolvedValue(mockTag);

      const result = await service.create(workspaceId, createDto);

      expect(result).toEqual(mockTag);
      expect(mockPrismaService.knowledgeTag.create).toHaveBeenCalledWith({
        data: {
          workspaceId,
          name: "Architecture",
          slug: "architecture",
          color: "#FF5733",
          description: "Architecture related topics",
        },
        select: {
          id: true,
          workspaceId: true,
          name: true,
          slug: true,
          color: true,
          description: true,
        },
      });
    });

    it("should create a tag with provided slug", async () => {
      const createDto: CreateTagDto = {
        name: "Architecture",
        slug: "arch",
        color: "#FF5733",
      };

      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue(null);
      mockPrismaService.knowledgeTag.create.mockResolvedValue({
        ...mockTag,
        slug: "arch",
      });

      const result = await service.create(workspaceId, createDto);

      expect(result.slug).toBe("arch");
    });

    it("should throw ConflictException if slug already exists", async () => {
      const createDto: CreateTagDto = {
        name: "Architecture",
      };

      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue(mockTag);

      await expect(service.create(workspaceId, createDto)).rejects.toThrow(ConflictException);
    });

    it("should throw BadRequestException for invalid slug format", async () => {
      const createDto: CreateTagDto = {
        name: "Architecture",
        slug: "Invalid_Slug!",
      };

      await expect(service.create(workspaceId, createDto)).rejects.toThrow(BadRequestException);
    });

    it("should generate slug from name with spaces and special chars", async () => {
      const createDto: CreateTagDto = {
        name: "My Tag Name!",
      };

      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue(null);
      mockPrismaService.knowledgeTag.create.mockImplementation(async ({ data }: any) => ({
        ...mockTag,
        slug: data.slug,
      }));

      const result = await service.create(workspaceId, createDto);

      expect(result.slug).toBe("my-tag-name");
    });
  });

  describe("findAll", () => {
    it("should return all tags for workspace", async () => {
      const mockTags = [
        { ...mockTag, _count: { entries: 5 } },
        {
          id: "tag-456",
          workspaceId,
          name: "Design",
          slug: "design",
          color: "#00FF00",
          description: null,
          _count: { entries: 3 },
        },
      ];

      mockPrismaService.knowledgeTag.findMany.mockResolvedValue(mockTags);

      const result = await service.findAll(workspaceId);

      expect(result).toEqual(mockTags);
      expect(mockPrismaService.knowledgeTag.findMany).toHaveBeenCalledWith({
        where: { workspaceId },
        include: {
          _count: {
            select: { entries: true },
          },
        },
        orderBy: { name: "asc" },
      });
    });
  });

  describe("findOne", () => {
    it("should return a tag by slug", async () => {
      const mockTagWithCount = { ...mockTag, _count: { entries: 5 } };
      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue(mockTagWithCount);

      const result = await service.findOne("architecture", workspaceId);

      expect(result).toEqual(mockTagWithCount);
      expect(mockPrismaService.knowledgeTag.findUnique).toHaveBeenCalledWith({
        where: {
          workspaceId_slug: {
            workspaceId,
            slug: "architecture",
          },
        },
        include: {
          _count: {
            select: { entries: true },
          },
        },
      });
    });

    it("should throw NotFoundException if tag not found", async () => {
      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue(null);

      await expect(service.findOne("nonexistent", workspaceId)).rejects.toThrow(NotFoundException);
    });
  });

  describe("update", () => {
    it("should update a tag", async () => {
      const updateDto: UpdateTagDto = {
        name: "Updated Architecture",
        color: "#0000FF",
      };

      mockPrismaService.knowledgeTag.findUnique
        .mockResolvedValueOnce({ ...mockTag, _count: { entries: 5 } })
        .mockResolvedValueOnce(null); // Check for slug conflict

      mockPrismaService.knowledgeTag.update.mockResolvedValue({
        ...mockTag,
        name: "Updated Architecture",
        slug: "updated-architecture",
        color: "#0000FF",
      });

      const result = await service.update("architecture", workspaceId, updateDto);

      expect(result.name).toBe("Updated Architecture");
      expect(result.slug).toBe("updated-architecture");
    });

    it("should throw NotFoundException if tag not found", async () => {
      const updateDto: UpdateTagDto = {
        name: "Updated",
      };

      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue(null);

      await expect(service.update("nonexistent", workspaceId, updateDto)).rejects.toThrow(
        NotFoundException
      );
    });

    it("should throw ConflictException if new slug conflicts", async () => {
      const updateDto: UpdateTagDto = {
        name: "Design", // Will generate "design" slug
      };

      mockPrismaService.knowledgeTag.findUnique
        .mockResolvedValueOnce({ ...mockTag, _count: { entries: 5 } })
        .mockResolvedValueOnce({
          // Conflict check
          id: "other-tag",
          slug: "design",
        } as any);

      await expect(service.update("architecture", workspaceId, updateDto)).rejects.toThrow(
        ConflictException
      );
    });
  });

  describe("remove", () => {
    it("should delete a tag", async () => {
      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue({
        ...mockTag,
        _count: { entries: 5 },
      });
      mockPrismaService.knowledgeTag.delete.mockResolvedValue(mockTag);

      await service.remove("architecture", workspaceId);

      expect(mockPrismaService.knowledgeTag.delete).toHaveBeenCalledWith({
        where: {
          workspaceId_slug: {
            workspaceId,
            slug: "architecture",
          },
        },
      });
    });

    it("should throw NotFoundException if tag not found", async () => {
      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue(null);

      await expect(service.remove("nonexistent", workspaceId)).rejects.toThrow(NotFoundException);
    });
  });

  describe("getEntriesWithTag", () => {
    it("should return entries with the tag", async () => {
      const mockEntries = [
        {
          id: "entry-1",
          slug: "entry-one",
          title: "Entry One",
          summary: "Summary",
          status: "PUBLISHED",
          visibility: "WORKSPACE",
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ];

      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue({
        ...mockTag,
        _count: { entries: 1 },
      });
      mockPrismaService.knowledgeEntry.findMany.mockResolvedValue(mockEntries);

      const result = await service.getEntriesWithTag("architecture", workspaceId);

      expect(result).toEqual(mockEntries);
      expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith({
        where: {
          workspaceId,
          tags: {
            some: {
              tagId: mockTag.id,
            },
          },
        },
        select: {
          id: true,
          slug: true,
          title: true,
          summary: true,
          status: true,
          visibility: true,
          createdAt: true,
          updatedAt: true,
        },
        orderBy: {
          updatedAt: "desc",
        },
      });
    });
  });

  describe("findOrCreateTags", () => {
    it("should find existing tags", async () => {
      const slugs = ["architecture", "design"];
      const mockTags = [
        { id: "tag-1", slug: "architecture", name: "Architecture" },
        { id: "tag-2", slug: "design", name: "Design" },
      ];

      mockPrismaService.knowledgeTag.findUnique
        .mockResolvedValueOnce(mockTags[0])
        .mockResolvedValueOnce(mockTags[1]);

      const result = await service.findOrCreateTags(workspaceId, slugs, false);

      expect(result).toEqual(mockTags);
    });

    it("should auto-create missing tags when autoCreate is true", async () => {
      const slugs = ["architecture", "new-tag"];

      mockPrismaService.knowledgeTag.findUnique
        .mockResolvedValueOnce({
          id: "tag-1",
          slug: "architecture",
          name: "Architecture",
        })
        .mockResolvedValueOnce(null) // new-tag doesn't exist
        .mockResolvedValueOnce(null); // Check for conflict during creation

      mockPrismaService.knowledgeTag.create.mockResolvedValue({
        id: "tag-2",
        workspaceId,
        slug: "new-tag",
        name: "New Tag",
        color: null,
        description: null,
      });

      const result = await service.findOrCreateTags(workspaceId, slugs, true);

      expect(result).toHaveLength(2);
      expect(result[1].slug).toBe("new-tag");
      expect(result[1].name).toBe("New Tag");
    });

    it("should throw NotFoundException when tag not found and autoCreate is false", async () => {
      const slugs = ["nonexistent"];

      mockPrismaService.knowledgeTag.findUnique.mockResolvedValue(null);

      await expect(service.findOrCreateTags(workspaceId, slugs, false)).rejects.toThrow(
        NotFoundException
      );
    });
  });
});
