/**
 * LayoutsService Unit Tests
 * Following TDD principles
 */

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { NotFoundException } from "@nestjs/common";
import { LayoutsService } from "../layouts.service";
import { PrismaService } from "../../prisma/prisma.service";

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

  const mockWorkspaceId = "workspace-123";
  const mockUserId = "user-123";

  const mockLayout = {
    id: "layout-1",
    workspaceId: mockWorkspaceId,
    userId: mockUserId,
    name: "Default Layout",
    isDefault: true,
    layout: [
      { i: "tasks-1", x: 0, y: 0, w: 2, h: 2 },
      { i: "calendar-1", x: 2, y: 0, w: 2, h: 2 },
    ],
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        LayoutsService,
        {
          provide: PrismaService,
          useValue: {
            userLayout: {
              findMany: vi.fn(),
              findFirst: vi.fn(),
              findUnique: vi.fn(),
              create: vi.fn(),
              update: vi.fn(),
              updateMany: vi.fn(),
              delete: vi.fn(),
            },
            $transaction: vi.fn((callback) => callback(prisma)),
          },
        },
      ],
    }).compile();

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

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe("findAll", () => {
    it("should return all layouts for a user", async () => {
      const mockLayouts = [mockLayout];
      prisma.userLayout.findMany.mockResolvedValue(mockLayouts);

      const result = await service.findAll(mockWorkspaceId, mockUserId);

      expect(result).toEqual(mockLayouts);
      expect(prisma.userLayout.findMany).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
        },
        orderBy: {
          isDefault: "desc",
          createdAt: "desc",
        },
      });
    });
  });

  describe("findDefault", () => {
    it("should return default layout", async () => {
      prisma.userLayout.findFirst.mockResolvedValueOnce(mockLayout);

      const result = await service.findDefault(mockWorkspaceId, mockUserId);

      expect(result).toEqual(mockLayout);
      expect(prisma.userLayout.findFirst).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          isDefault: true,
        },
      });
    });

    it("should return most recent layout if no default exists", async () => {
      prisma.userLayout.findFirst
        .mockResolvedValueOnce(null) // No default
        .mockResolvedValueOnce(mockLayout); // Most recent

      const result = await service.findDefault(mockWorkspaceId, mockUserId);

      expect(result).toEqual(mockLayout);
      expect(prisma.userLayout.findFirst).toHaveBeenCalledTimes(2);
    });

    it("should throw NotFoundException if no layouts exist", async () => {
      prisma.userLayout.findFirst
        .mockResolvedValueOnce(null) // No default
        .mockResolvedValueOnce(null); // No layouts

      await expect(service.findDefault(mockWorkspaceId, mockUserId)).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("findOne", () => {
    it("should return a layout by ID", async () => {
      prisma.userLayout.findUnique.mockResolvedValue(mockLayout);

      const result = await service.findOne("layout-1", mockWorkspaceId, mockUserId);

      expect(result).toEqual(mockLayout);
      expect(prisma.userLayout.findUnique).toHaveBeenCalledWith({
        where: {
          id: "layout-1",
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
        },
      });
    });

    it("should throw NotFoundException if layout not found", async () => {
      prisma.userLayout.findUnique.mockResolvedValue(null);

      await expect(service.findOne("invalid-id", mockWorkspaceId, mockUserId)).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("create", () => {
    it("should create a new layout", async () => {
      const createDto = {
        name: "New Layout",
        layout: [],
        isDefault: false,
      };

      prisma.$transaction.mockImplementation((callback) =>
        callback({
          userLayout: {
            create: vi.fn().mockResolvedValue(mockLayout),
            updateMany: vi.fn(),
          },
        })
      );

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

      expect(result).toBeDefined();
    });

    it("should unset other defaults when creating default layout", async () => {
      const createDto = {
        name: "New Default",
        layout: [],
        isDefault: true,
      };

      const mockUpdateMany = vi.fn();
      const mockCreate = vi.fn().mockResolvedValue(mockLayout);

      prisma.$transaction.mockImplementation((callback) =>
        callback({
          userLayout: {
            updateMany: mockUpdateMany,
            create: mockCreate,
          },
        })
      );

      await service.create(mockWorkspaceId, mockUserId, createDto);

      expect(mockUpdateMany).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          isDefault: true,
        },
        data: {
          isDefault: false,
        },
      });
    });
  });

  describe("update", () => {
    it("should update a layout", async () => {
      const updateDto = {
        name: "Updated Layout",
        layout: [{ i: "tasks-1", x: 1, y: 0, w: 2, h: 2 }],
      };

      const mockUpdate = vi.fn().mockResolvedValue({ ...mockLayout, ...updateDto });
      const mockFindUnique = vi.fn().mockResolvedValue(mockLayout);

      prisma.$transaction.mockImplementation((callback) =>
        callback({
          userLayout: {
            findUnique: mockFindUnique,
            update: mockUpdate,
            updateMany: vi.fn(),
          },
        })
      );

      const result = await service.update("layout-1", mockWorkspaceId, mockUserId, updateDto);

      expect(result).toBeDefined();
      expect(mockFindUnique).toHaveBeenCalled();
      expect(mockUpdate).toHaveBeenCalled();
    });

    it("should throw NotFoundException if layout not found", async () => {
      const mockFindUnique = vi.fn().mockResolvedValue(null);

      prisma.$transaction.mockImplementation((callback) =>
        callback({
          userLayout: {
            findUnique: mockFindUnique,
          },
        })
      );

      await expect(service.update("invalid-id", mockWorkspaceId, mockUserId, {})).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("remove", () => {
    it("should delete a layout", async () => {
      prisma.userLayout.findUnique.mockResolvedValue(mockLayout);
      prisma.userLayout.delete.mockResolvedValue(mockLayout);

      await service.remove("layout-1", mockWorkspaceId, mockUserId);

      expect(prisma.userLayout.delete).toHaveBeenCalledWith({
        where: {
          id: "layout-1",
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
        },
      });
    });

    it("should throw NotFoundException if layout not found", async () => {
      prisma.userLayout.findUnique.mockResolvedValue(null);

      await expect(service.remove("invalid-id", mockWorkspaceId, mockUserId)).rejects.toThrow(
        NotFoundException
      );
    });
  });
});
