import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { WorkspaceSettingsService } from "./workspace-settings.service";
import { PrismaService } from "../prisma/prisma.service";
import type { WorkspaceLlmSettings, LlmProviderInstance, Personality } from "@prisma/client";

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

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

  const mockSettings: WorkspaceLlmSettings = {
    id: "settings-123",
    workspaceId: mockWorkspaceId,
    defaultLlmProviderId: "provider-123",
    defaultPersonalityId: "personality-123",
    settings: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockProvider: LlmProviderInstance = {
    id: "provider-123",
    providerType: "ollama",
    displayName: "Test Provider",
    userId: null,
    config: { endpoint: "http://localhost:11434" },
    isDefault: true,
    isEnabled: true,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockUserProvider: LlmProviderInstance = {
    id: "user-provider-123",
    providerType: "ollama",
    displayName: "User Provider",
    userId: mockUserId,
    config: { endpoint: "http://user-ollama:11434" },
    isDefault: false,
    isEnabled: true,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockPersonality: Personality = {
    id: "personality-123",
    workspaceId: mockWorkspaceId,
    name: "default",
    displayName: "Default",
    description: "Default personality",
    systemPrompt: "You are a helpful assistant",
    temperature: null,
    maxTokens: null,
    llmProviderInstanceId: null,
    isDefault: true,
    isEnabled: true,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        WorkspaceSettingsService,
        {
          provide: PrismaService,
          useValue: {
            workspaceLlmSettings: {
              findUnique: vi.fn(),
              create: vi.fn(),
              update: vi.fn(),
            },
            llmProviderInstance: {
              findFirst: vi.fn(),
              findUnique: vi.fn(),
            },
            personality: {
              findFirst: vi.fn(),
              findUnique: vi.fn(),
            },
          },
        },
      ],
    }).compile();

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

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

  describe("getSettings", () => {
    it("should return existing settings for workspace", async () => {
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);

      const result = await service.getSettings(mockWorkspaceId);

      expect(result).toEqual(mockSettings);
      expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledWith({
        where: { workspaceId: mockWorkspaceId },
      });
    });

    it("should create default settings if not exists", async () => {
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(null);
      vi.spyOn(prisma.workspaceLlmSettings, "create").mockResolvedValue(mockSettings);

      const result = await service.getSettings(mockWorkspaceId);

      expect(result).toEqual(mockSettings);
      expect(prisma.workspaceLlmSettings.create).toHaveBeenCalledWith({
        data: {
          workspaceId: mockWorkspaceId,
          settings: {},
        },
      });
    });

    it("should handle workspace with no settings gracefully", async () => {
      const newSettings = {
        ...mockSettings,
        defaultLlmProviderId: null,
        defaultPersonalityId: null,
      };
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(null);
      vi.spyOn(prisma.workspaceLlmSettings, "create").mockResolvedValue(newSettings);

      const result = await service.getSettings(mockWorkspaceId);

      expect(result).toBeDefined();
      expect(result.workspaceId).toBe(mockWorkspaceId);
    });
  });

  describe("updateSettings", () => {
    it("should update existing settings", async () => {
      const updateDto = {
        defaultLlmProviderId: "new-provider-123",
        defaultPersonalityId: "new-personality-123",
      };

      const updatedSettings = { ...mockSettings, ...updateDto };
      vi.spyOn(prisma.workspaceLlmSettings, "update").mockResolvedValue(updatedSettings);

      const result = await service.updateSettings(mockWorkspaceId, updateDto);

      expect(result).toEqual(updatedSettings);
      expect(prisma.workspaceLlmSettings.update).toHaveBeenCalledWith({
        where: { workspaceId: mockWorkspaceId },
        data: updateDto,
      });
    });

    it("should allow setting provider to null", async () => {
      const updateDto = {
        defaultLlmProviderId: null,
      };

      const updatedSettings = { ...mockSettings, defaultLlmProviderId: null };
      vi.spyOn(prisma.workspaceLlmSettings, "update").mockResolvedValue(updatedSettings);

      const result = await service.updateSettings(mockWorkspaceId, updateDto);

      expect(result.defaultLlmProviderId).toBeNull();
    });

    it("should allow setting personality to null", async () => {
      const updateDto = {
        defaultPersonalityId: null,
      };

      const updatedSettings = { ...mockSettings, defaultPersonalityId: null };
      vi.spyOn(prisma.workspaceLlmSettings, "update").mockResolvedValue(updatedSettings);

      const result = await service.updateSettings(mockWorkspaceId, updateDto);

      expect(result.defaultPersonalityId).toBeNull();
    });

    it("should update custom settings object", async () => {
      const updateDto = {
        settings: { customKey: "customValue" },
      };

      const updatedSettings = { ...mockSettings, settings: updateDto.settings };
      vi.spyOn(prisma.workspaceLlmSettings, "update").mockResolvedValue(updatedSettings);

      const result = await service.updateSettings(mockWorkspaceId, updateDto);

      expect(result.settings).toEqual(updateDto.settings);
    });
  });

  describe("getEffectiveLlmProvider", () => {
    it("should return workspace provider when set", async () => {
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
      vi.spyOn(prisma.llmProviderInstance, "findUnique").mockResolvedValue(mockProvider);

      const result = await service.getEffectiveLlmProvider(mockWorkspaceId);

      expect(result).toEqual(mockProvider);
      expect(prisma.llmProviderInstance.findUnique).toHaveBeenCalledWith({
        where: { id: mockSettings.defaultLlmProviderId! },
      });
    });

    it("should return user provider when workspace provider not set and userId provided", async () => {
      const settingsWithoutProvider = { ...mockSettings, defaultLlmProviderId: null };
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
        settingsWithoutProvider
      );
      vi.spyOn(prisma.llmProviderInstance, "findFirst")
        .mockResolvedValueOnce(mockUserProvider)
        .mockResolvedValueOnce(null);

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

      expect(result).toEqual(mockUserProvider);
      expect(prisma.llmProviderInstance.findFirst).toHaveBeenCalledWith({
        where: {
          userId: mockUserId,
          isEnabled: true,
        },
      });
    });

    it("should fall back to system default when workspace and user providers not set", async () => {
      const settingsWithoutProvider = { ...mockSettings, defaultLlmProviderId: null };
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
        settingsWithoutProvider
      );
      vi.spyOn(prisma.llmProviderInstance, "findFirst")
        .mockResolvedValueOnce(null) // No user provider
        .mockResolvedValueOnce(mockProvider); // System default

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

      expect(result).toEqual(mockProvider);
      expect(prisma.llmProviderInstance.findFirst).toHaveBeenNthCalledWith(2, {
        where: {
          userId: null,
          isDefault: true,
          isEnabled: true,
        },
      });
    });

    it("should throw error when no provider available", async () => {
      const settingsWithoutProvider = { ...mockSettings, defaultLlmProviderId: null };
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
        settingsWithoutProvider
      );
      vi.spyOn(prisma.llmProviderInstance, "findFirst").mockResolvedValue(null);

      await expect(service.getEffectiveLlmProvider(mockWorkspaceId)).rejects.toThrow(
        `No LLM provider available for workspace ${mockWorkspaceId}`
      );
    });

    it("should throw error when workspace provider is set but not found", async () => {
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
      vi.spyOn(prisma.llmProviderInstance, "findUnique").mockResolvedValue(null);

      await expect(service.getEffectiveLlmProvider(mockWorkspaceId)).rejects.toThrow(
        `LLM provider ${mockSettings.defaultLlmProviderId} not found`
      );
    });
  });

  describe("getEffectivePersonality", () => {
    it("should return workspace personality when set", async () => {
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
      vi.spyOn(prisma.personality, "findUnique").mockResolvedValue(mockPersonality);

      const result = await service.getEffectivePersonality(mockWorkspaceId);

      expect(result).toEqual(mockPersonality);
      expect(prisma.personality.findUnique).toHaveBeenCalledWith({
        where: {
          id: mockSettings.defaultPersonalityId!,
        },
      });
    });

    it("should fall back to default personality when workspace personality not set", async () => {
      const settingsWithoutPersonality = { ...mockSettings, defaultPersonalityId: null };
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
        settingsWithoutPersonality
      );
      vi.spyOn(prisma.personality, "findFirst").mockResolvedValue(mockPersonality);

      const result = await service.getEffectivePersonality(mockWorkspaceId);

      expect(result).toEqual(mockPersonality);
      expect(prisma.personality.findFirst).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          isDefault: true,
          isEnabled: true,
        },
      });
    });

    it("should fall back to any enabled personality when no default exists", async () => {
      const settingsWithoutPersonality = { ...mockSettings, defaultPersonalityId: null };
      const nonDefaultPersonality = { ...mockPersonality, isDefault: false };

      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
        settingsWithoutPersonality
      );
      vi.spyOn(prisma.personality, "findFirst")
        .mockResolvedValueOnce(null) // No default personality
        .mockResolvedValueOnce(nonDefaultPersonality); // Any enabled personality

      const result = await service.getEffectivePersonality(mockWorkspaceId);

      expect(result).toEqual(nonDefaultPersonality);
      expect(prisma.personality.findFirst).toHaveBeenNthCalledWith(2, {
        where: {
          workspaceId: mockWorkspaceId,
          isEnabled: true,
        },
      });
    });

    it("should throw error when no personality available", async () => {
      const settingsWithoutPersonality = { ...mockSettings, defaultPersonalityId: null };
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
        settingsWithoutPersonality
      );
      vi.spyOn(prisma.personality, "findFirst").mockResolvedValue(null);

      await expect(service.getEffectivePersonality(mockWorkspaceId)).rejects.toThrow(
        `No personality available for workspace ${mockWorkspaceId}`
      );
    });

    it("should throw error when workspace personality is set but not found", async () => {
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
      vi.spyOn(prisma.personality, "findUnique").mockResolvedValue(null);

      await expect(service.getEffectivePersonality(mockWorkspaceId)).rejects.toThrow(
        `Personality ${mockSettings.defaultPersonalityId} not found`
      );
    });
  });

  describe("workspace isolation", () => {
    it("should only access settings for specified workspace", async () => {
      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);

      await service.getSettings(mockWorkspaceId);

      expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledWith({
        where: { workspaceId: mockWorkspaceId },
      });
    });

    it("should not allow cross-workspace settings access", async () => {
      const otherWorkspaceId = "other-workspace-123";

      vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(null);

      const result1 = await service.getSettings(mockWorkspaceId);
      const result2 = await service.getSettings(otherWorkspaceId);

      // Each workspace should have separate calls
      expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledTimes(2);
      expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledWith({
        where: { workspaceId: mockWorkspaceId },
      });
      expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledWith({
        where: { workspaceId: otherWorkspaceId },
      });
    });
  });
});
