import { describe, it, expect, beforeEach, vi } from "vitest";
import { OllamaEmbeddingService } from "./ollama-embedding.service";
import { PrismaService } from "../../prisma/prisma.service";
import { OllamaService } from "../../ollama/ollama.service";
import { Test, TestingModule } from "@nestjs/testing";

describe("OllamaEmbeddingService", () => {
  let service: OllamaEmbeddingService;
  let prismaService: PrismaService;
  let ollamaService: OllamaService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        OllamaEmbeddingService,
        {
          provide: PrismaService,
          useValue: {
            $executeRaw: vi.fn(),
            knowledgeEmbedding: {
              deleteMany: vi.fn(),
              findUnique: vi.fn(),
            },
          },
        },
        {
          provide: OllamaService,
          useValue: {
            embed: vi.fn(),
            healthCheck: vi.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<OllamaEmbeddingService>(OllamaEmbeddingService);
    prismaService = module.get<PrismaService>(PrismaService);
    ollamaService = module.get<OllamaService>(OllamaService);
  });

  describe("isConfigured", () => {
    it("should return true when Ollama service is available", async () => {
      vi.spyOn(ollamaService, "healthCheck").mockResolvedValue({
        status: "healthy",
        mode: "local",
        endpoint: "http://localhost:11434",
        available: true,
      });

      const result = await service.isConfigured();

      expect(result).toBe(true);
    });

    it("should return false when Ollama service is unavailable", async () => {
      vi.spyOn(ollamaService, "healthCheck").mockResolvedValue({
        status: "unhealthy",
        mode: "local",
        endpoint: "http://localhost:11434",
        available: false,
        error: "Connection refused",
      });

      const result = await service.isConfigured();

      expect(result).toBe(false);
    });
  });

  describe("generateEmbedding", () => {
    it("should generate embedding vector from text", async () => {
      const mockEmbedding = new Array(1536).fill(0).map((_, i) => i / 1536);
      vi.spyOn(ollamaService, "embed").mockResolvedValue({
        embedding: mockEmbedding,
      });

      const result = await service.generateEmbedding("test text");

      expect(result).toEqual(mockEmbedding);
      expect(ollamaService.embed).toHaveBeenCalledWith("test text", "mxbai-embed-large");
    });

    it("should use custom model when provided", async () => {
      const mockEmbedding = new Array(1536).fill(0).map((_, i) => i / 1536);
      vi.spyOn(ollamaService, "embed").mockResolvedValue({
        embedding: mockEmbedding,
      });

      await service.generateEmbedding("test text", { model: "custom-model" });

      expect(ollamaService.embed).toHaveBeenCalledWith("test text", "custom-model");
    });

    it("should throw error when Ollama service fails", async () => {
      vi.spyOn(ollamaService, "embed").mockRejectedValue(new Error("Ollama unavailable"));

      await expect(service.generateEmbedding("test text")).rejects.toThrow("Ollama unavailable");
    });
  });

  describe("generateAndStoreEmbedding", () => {
    it("should generate and store embedding for entry", async () => {
      const mockEmbedding = new Array(1536).fill(0).map((_, i) => i / 1536);
      vi.spyOn(ollamaService, "healthCheck").mockResolvedValue({
        status: "healthy",
        mode: "local",
        endpoint: "http://localhost:11434",
        available: true,
      });
      vi.spyOn(ollamaService, "embed").mockResolvedValue({
        embedding: mockEmbedding,
      });
      vi.spyOn(prismaService, "$executeRaw").mockResolvedValue(1);

      await service.generateAndStoreEmbedding("entry-123", "test content");

      expect(ollamaService.embed).toHaveBeenCalledWith("test content", "mxbai-embed-large");
      expect(prismaService.$executeRaw).toHaveBeenCalled();
    });

    it("should use custom model when provided", async () => {
      const mockEmbedding = new Array(1536).fill(0).map((_, i) => i / 1536);
      vi.spyOn(ollamaService, "healthCheck").mockResolvedValue({
        status: "healthy",
        mode: "local",
        endpoint: "http://localhost:11434",
        available: true,
      });
      vi.spyOn(ollamaService, "embed").mockResolvedValue({
        embedding: mockEmbedding,
      });
      vi.spyOn(prismaService, "$executeRaw").mockResolvedValue(1);

      await service.generateAndStoreEmbedding("entry-123", "test content", {
        model: "custom-model",
      });

      expect(ollamaService.embed).toHaveBeenCalledWith("test content", "custom-model");
    });

    it("should skip when Ollama is not configured", async () => {
      vi.spyOn(ollamaService, "healthCheck").mockResolvedValue({
        status: "unhealthy",
        mode: "local",
        endpoint: "http://localhost:11434",
        available: false,
        error: "Connection refused",
      });

      await service.generateAndStoreEmbedding("entry-123", "test content");

      expect(ollamaService.embed).not.toHaveBeenCalled();
      expect(prismaService.$executeRaw).not.toHaveBeenCalled();
    });
  });

  describe("deleteEmbedding", () => {
    it("should delete embedding for entry", async () => {
      vi.spyOn(prismaService.knowledgeEmbedding, "deleteMany").mockResolvedValue({
        count: 1,
      });

      await service.deleteEmbedding("entry-123");

      expect(prismaService.knowledgeEmbedding.deleteMany).toHaveBeenCalledWith({
        where: { entryId: "entry-123" },
      });
    });
  });

  describe("prepareContentForEmbedding", () => {
    it("should combine title and content with title weighting", () => {
      const title = "Test Title";
      const content = "Test content goes here";

      const result = service.prepareContentForEmbedding(title, content);

      expect(result).toContain(title);
      expect(result).toContain(content);
      // Title should appear twice for weighting
      expect(result.split(title).length - 1).toBe(2);
    });

    it("should handle empty content", () => {
      const title = "Test Title";
      const content = "";

      const result = service.prepareContentForEmbedding(title, content);

      expect(result).toBe(`${title}\n\n${title}`);
    });
  });

  describe("hasEmbedding", () => {
    it("should return true when entry has embedding", async () => {
      vi.spyOn(prismaService.knowledgeEmbedding, "findUnique").mockResolvedValue({
        id: "embedding-123",
        entryId: "entry-123",
        embedding: "[0.1,0.2,0.3]",
        model: "mxbai-embed-large",
        createdAt: new Date(),
        updatedAt: new Date(),
      } as never);

      const result = await service.hasEmbedding("entry-123");

      expect(result).toBe(true);
    });

    it("should return false when entry has no embedding", async () => {
      vi.spyOn(prismaService.knowledgeEmbedding, "findUnique").mockResolvedValue(null);

      const result = await service.hasEmbedding("entry-123");

      expect(result).toBe(false);
    });
  });
});
