// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { ensureQdrantReady } from "../../src/services/docker.js";
import { ensureOllamaReady } from "../../src/services/ollama.js";
import {
  deleteCollection,
  deleteFileChunks,
  deleteProjectMetadata,
  ensureCollection,
  getCollectionInfo,
  getProjectMetadata,
  listCodebaseCollections,
  loadProjectHashes,
  saveProjectMetadata,
  searchChunks,
  upsertChunks,
} from "../../src/services/qdrant.js";
import type { FileChunk } from "../../src/types.js";
import { isDockerAvailable } from "../helpers/fixtures.js";
import { deleteTestCollection, waitForOllama, waitForQdrant } from "../helpers/setup.js";

const dockerAvailable = isDockerAvailable();
const TEST_COLLECTION = "codebase_test_qdrant_integration";

describe.skipIf(!dockerAvailable)("qdrant service", () => {
  beforeAll(async () => {
    await ensureQdrantReady();
    await ensureOllamaReady();
    await waitForQdrant();
    await waitForOllama();

    // Clean up from any previous test run
    await deleteTestCollection(TEST_COLLECTION);
  });

  afterAll(async () => {
    await deleteTestCollection(TEST_COLLECTION);
  });

  describe("collection management", () => {
    it("creates a collection with correct dimensions", async () => {
      await ensureCollection(TEST_COLLECTION);
      const info = await getCollectionInfo(TEST_COLLECTION);

      expect(info).toBeDefined();
      expect(info?.status).toBe("green");
    });

    it("is idempotent — creating an existing collection does not error", async () => {
      await expect(ensureCollection(TEST_COLLECTION)).resolves.not.toThrow();
    });

    it("lists collections including the test collection", async () => {
      const collections = await listCodebaseCollections();
      expect(collections).toContain(TEST_COLLECTION);
    });

    it("returns collection info", async () => {
      const info = await getCollectionInfo(TEST_COLLECTION);
      expect(info).toBeDefined();
      expect(info?.pointsCount).toBe(0); // no data yet
    });

    it("returns null info for non-existent collection", async () => {
      const info = await getCollectionInfo("nonexistent_collection_xyz");
      expect(info).toBeNull();
    });
  });

  describe("chunk upsert and search with real embeddings", () => {
    const chunks: FileChunk[] = [
      {
        id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
        filePath: "/project/src/auth.ts",
        relativePath: "src/auth.ts",
        content:
          "export function authenticateUser(token: string): boolean {\n  // Validate JWT token and check permissions\n  const parts = token.split('.');\n  return parts.length === 3;\n}",
        startLine: 1,
        endLine: 5,
        language: "typescript",
        type: "code",
      },
      {
        id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
        filePath: "/project/src/math.ts",
        relativePath: "src/math.ts",
        content:
          "export function fibonacci(n: number): number {\n  if (n <= 0) return 0;\n  if (n === 1) return 1;\n  let prev = 0, curr = 1;\n  for (let i = 2; i <= n; i++) {\n    [prev, curr] = [curr, prev + curr];\n  }\n  return curr;\n}",
        startLine: 1,
        endLine: 9,
        language: "typescript",
        type: "code",
      },
      {
        id: "cccccccc-cccc-cccc-cccc-cccccccccccc",
        filePath: "/project/lib/data.py",
        relativePath: "lib/data.py",
        content:
          'def load_json_file(filepath: str) -> dict:\n    """Load and parse a JSON file from disk."""\n    with open(filepath, "r") as f:\n        return json.load(f)',
        startLine: 1,
        endLine: 4,
        language: "python",
        type: "code",
      },
    ];

    const _embeddings: number[][] = [];

    it("upserts chunks with real embeddings (generated internally)", async () => {
      await upsertChunks(TEST_COLLECTION, chunks, "test-content-hash");

      // Verify points were created
      const info = await getCollectionInfo(TEST_COLLECTION);
      expect(info).toBeDefined();
      expect(info?.pointsCount).toBe(3);
    });

    it("searches for authentication-related code", async () => {
      const results = await searchChunks(
        TEST_COLLECTION,
        "user authentication with JWT tokens",
        5,
      );

      expect(results.length).toBeGreaterThan(0);
      // The auth.ts chunk should rank highest for auth-related queries
      expect(results[0].relativePath).toBe("src/auth.ts");
      expect(results[0].score).toBeGreaterThan(0);
      expect(results[0].content).toContain("authenticateUser");
    });

    it("searches for mathematical algorithms", async () => {
      const results = await searchChunks(
        TEST_COLLECTION,
        "fibonacci sequence calculation algorithm",
        5,
      );

      expect(results.length).toBeGreaterThan(0);
      // The math.ts chunk should rank highest for math queries
      expect(results[0].relativePath).toBe("src/math.ts");
      expect(results[0].content).toContain("fibonacci");
    });

    it("searches for data loading utilities", async () => {
      const results = await searchChunks(
        TEST_COLLECTION,
        "load and parse JSON data from file",
        5,
      );

      expect(results.length).toBeGreaterThan(0);
      // The data.py chunk should rank highest
      expect(results[0].relativePath).toBe("lib/data.py");
    });

    it("supports limit parameter", async () => {
      const results = await searchChunks(TEST_COLLECTION, "code", 2);
      expect(results.length).toBeLessThanOrEqual(2);
    });

    it("supports file filter", async () => {
      const results = await searchChunks(
        TEST_COLLECTION,
        "function",
        10,
        "auth",
      );

      // Only auth.ts should match the file filter
      for (const r of results) {
        expect(r.relativePath).toContain("auth");
      }
    });

    it("supports language filter", async () => {
      const results = await searchChunks(
        TEST_COLLECTION,
        "function",
        10,
        undefined,
        "python",
      );

      for (const r of results) {
        expect(r.language).toBe("python");
      }
    });

    it("returns empty results for unrelated queries in filtered search", async () => {
      const results = await searchChunks(
        TEST_COLLECTION,
        "authentication",
        10,
        undefined,
        "python",
      );

      // Should return python results (only data.py), even though
      // the query is more related to auth.ts
      for (const r of results) {
        expect(r.language).toBe("python");
      }
    });
  });

  describe("delete file chunks", () => {
    it("deletes chunks for a specific file", async () => {
      await deleteFileChunks(TEST_COLLECTION, "lib/data.py");

      const info = await getCollectionInfo(TEST_COLLECTION);
      expect(info?.pointsCount).toBe(2); // 2 remaining
    });
  });

  describe("metadata collection", () => {
    const metadataCollection = TEST_COLLECTION; // reuse the test collection for metadata
    const projectPath = "/test/project/path";

    it("saves project metadata", async () => {
      const fileHashes = new Map<string, string>([
        ["src/auth.ts", "hash-auth"],
        ["src/math.ts", "hash-math"],
      ]);

      await saveProjectMetadata(metadataCollection, projectPath, 42, 2, fileHashes, "completed");

      const metadata = await getProjectMetadata(metadataCollection);
      expect(metadata).toBeDefined();
      expect(metadata?.projectPath).toBe(projectPath);
      expect(metadata?.filesTotal).toBe(42);
      expect(metadata?.filesIndexed).toBe(2);
    });

    it("loads project hashes", async () => {
      const hashes = await loadProjectHashes(metadataCollection);
      // Initially might be empty or contain entries based on implementation
      expect(hashes).toBeDefined();
      expect(typeof hashes).toBe("object");
    });

    it("can delete project metadata", async () => {
      await deleteProjectMetadata(metadataCollection);
      // After deletion, metadata should be gone
      const metadata = await getProjectMetadata(metadataCollection);
      expect(metadata).toBeNull();
    });
  });

  describe("collection deletion", () => {
    it("deletes the test collection", async () => {
      await deleteCollection(TEST_COLLECTION);

      const info = await getCollectionInfo(TEST_COLLECTION);
      expect(info).toBeNull();
    });

    it("does not error when deleting non-existent collection", async () => {
      await expect(
        deleteCollection("nonexistent_collection_xyz_test"),
      ).resolves.not.toThrow();
    });
  });
});
