// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import { describe, expect, it } from "vitest";
import {
  CHUNK_OVERLAP,
  CHUNK_SIZE,
  getLanguageFromExtension,
  INDEX_BATCH_SIZE,
  MAX_AVG_LINE_LENGTH,
  MAX_CHUNK_CHARS,
  MAX_FILE_BYTES,
  mergeExtraExtensions,
  OLLAMA_CONTAINER_NAME,
  OLLAMA_HOST,
  OLLAMA_IMAGE,
  OLLAMA_PORT,
  parseExtraExtensions,
  QDRANT_CONTAINER_NAME,
  QDRANT_GRPC_PORT,
  QDRANT_HOST,
  QDRANT_IMAGE,
  QDRANT_MODE,
  QDRANT_PORT,
  QDRANT_URL,
  resolveQdrantPort,
  SEARCH_DEFAULT_LIMIT,
  SEARCH_MIN_SCORE,
  SPECIAL_FILES,
  SUPPORTED_EXTENSIONS,
} from "../../src/constants.js";

describe("constants", () => {
  describe("Qdrant configuration", () => {
    it("has default port 16333", () => {
      expect(QDRANT_PORT).toBe(16333);
    });

    it("has default gRPC port 16334", () => {
      expect(QDRANT_GRPC_PORT).toBe(16334);
    });

    it("defaults host to localhost", () => {
      expect(QDRANT_HOST).toBe("localhost");
    });

    it("has correct container name", () => {
      expect(QDRANT_CONTAINER_NAME).toBe("socraticode-qdrant");
    });

    it("uses pinned Qdrant image version", () => {
      expect(QDRANT_IMAGE).toMatch(/^qdrant\/qdrant:v\d+\.\d+\.\d+$/);
    });

    it("defaults mode to managed", () => {
      expect(QDRANT_MODE).toBe("managed");
    });

    it("defaults URL to undefined", () => {
      expect(QDRANT_URL).toBeUndefined();
    });
  });

  describe("Ollama configuration", () => {
    it("has default port 11435", () => {
      expect(OLLAMA_PORT).toBe(11435);
    });

    it("defaults host to localhost with configured port", () => {
      expect(OLLAMA_HOST).toBe(`http://localhost:${OLLAMA_PORT}`);
    });

    it("has correct container name", () => {
      expect(OLLAMA_CONTAINER_NAME).toBe("socraticode-ollama");
    });

    it("uses latest Ollama image", () => {
      expect(OLLAMA_IMAGE).toBe("ollama/ollama:latest");
    });
  });

  describe("chunking configuration", () => {
    it("has reasonable chunk size", () => {
      expect(CHUNK_SIZE).toBeGreaterThan(0);
      expect(CHUNK_SIZE).toBeLessThanOrEqual(500);
    });

    it("has overlap smaller than chunk size", () => {
      expect(CHUNK_OVERLAP).toBeGreaterThan(0);
      expect(CHUNK_OVERLAP).toBeLessThan(CHUNK_SIZE);
    });

    it("defaults to 100 lines per chunk", () => {
      expect(CHUNK_SIZE).toBe(100);
    });

    it("defaults to 10 lines overlap", () => {
      expect(CHUNK_OVERLAP).toBe(10);
    });
  });

  describe("search configuration", () => {
    it("defaults SEARCH_DEFAULT_LIMIT to 10", () => {
      expect(SEARCH_DEFAULT_LIMIT).toBe(10);
    });

    it("clamps SEARCH_DEFAULT_LIMIT between 1 and 50", () => {
      // Can't easily test env override in-process, but verify the default is within bounds
      expect(SEARCH_DEFAULT_LIMIT).toBeGreaterThanOrEqual(1);
      expect(SEARCH_DEFAULT_LIMIT).toBeLessThanOrEqual(50);
    });

    it("defaults SEARCH_MIN_SCORE to 0.10", () => {
      expect(SEARCH_MIN_SCORE).toBeCloseTo(0.10);
    });

    it("clamps SEARCH_MIN_SCORE between 0 and 1", () => {
      expect(SEARCH_MIN_SCORE).toBeGreaterThanOrEqual(0);
      expect(SEARCH_MIN_SCORE).toBeLessThanOrEqual(1);
    });
  });

  describe("indexing configuration", () => {
    it("defaults INDEX_BATCH_SIZE to 50", () => {
      expect(INDEX_BATCH_SIZE).toBe(50);
    });

    it("defaults MAX_FILE_BYTES to 5MB", () => {
      expect(MAX_FILE_BYTES).toBe(5_000_000);
    });

    it("MAX_AVG_LINE_LENGTH is a positive number", () => {
      expect(MAX_AVG_LINE_LENGTH).toBeGreaterThan(0);
    });

    it("defaults MAX_AVG_LINE_LENGTH to 500", () => {
      expect(MAX_AVG_LINE_LENGTH).toBe(500);
    });

    it("MAX_CHUNK_CHARS is a positive number", () => {
      expect(MAX_CHUNK_CHARS).toBeGreaterThan(0);
    });

    it("defaults MAX_CHUNK_CHARS to 2000", () => {
      expect(MAX_CHUNK_CHARS).toBe(2000);
    });

    it("MAX_CHUNK_CHARS is less than MAX_AVG_LINE_LENGTH * CHUNK_SIZE (sanity check)", () => {
      // Ensures MAX_CHUNK_CHARS is a tighter limit than unconstrained line-based chunks
      expect(MAX_CHUNK_CHARS).toBeLessThan(MAX_AVG_LINE_LENGTH * CHUNK_SIZE);
    });
  });

  describe("SUPPORTED_EXTENSIONS", () => {
    it("is a Set", () => {
      expect(SUPPORTED_EXTENSIONS).toBeInstanceOf(Set);
    });

    it("contains common programming language extensions", () => {
      const expected = [".js", ".ts", ".py", ".java", ".go", ".rs", ".rb", ".php", ".swift", ".c", ".cpp"];
      for (const ext of expected) {
        expect(SUPPORTED_EXTENSIONS.has(ext), `missing: ${ext}`).toBe(true);
      }
    });

    it("contains TypeScript variants", () => {
      expect(SUPPORTED_EXTENSIONS.has(".ts")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".tsx")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".mjs")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".cjs")).toBe(true);
    });

    it("contains web extensions", () => {
      expect(SUPPORTED_EXTENSIONS.has(".html")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".css")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".scss")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".vue")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".svelte")).toBe(true);
    });

    it("contains config/data extensions", () => {
      expect(SUPPORTED_EXTENSIONS.has(".json")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".yaml")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".yml")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".toml")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".xml")).toBe(true);
    });

    it("contains documentation extensions", () => {
      expect(SUPPORTED_EXTENSIONS.has(".md")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".mdx")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".rst")).toBe(true);
      expect(SUPPORTED_EXTENSIONS.has(".txt")).toBe(true);
    });

    it("does not contain binary or image extensions", () => {
      expect(SUPPORTED_EXTENSIONS.has(".png")).toBe(false);
      expect(SUPPORTED_EXTENSIONS.has(".jpg")).toBe(false);
      expect(SUPPORTED_EXTENSIONS.has(".exe")).toBe(false);
      expect(SUPPORTED_EXTENSIONS.has(".pdf")).toBe(false);
    });

    it("has at least 50 extensions", () => {
      expect(SUPPORTED_EXTENSIONS.size).toBeGreaterThanOrEqual(50);
    });
  });

  describe("SPECIAL_FILES", () => {
    it("is a Set", () => {
      expect(SPECIAL_FILES).toBeInstanceOf(Set);
    });

    it("includes Dockerfile", () => {
      expect(SPECIAL_FILES.has("Dockerfile")).toBe(true);
    });

    it("includes Makefile", () => {
      expect(SPECIAL_FILES.has("Makefile")).toBe(true);
    });

    it("includes Gemfile", () => {
      expect(SPECIAL_FILES.has("Gemfile")).toBe(true);
    });

    it("includes .gitignore", () => {
      expect(SPECIAL_FILES.has(".gitignore")).toBe(true);
    });

    it("includes .env.example", () => {
      expect(SPECIAL_FILES.has(".env.example")).toBe(true);
    });
  });

  describe("getLanguageFromExtension", () => {
    it("maps .ts to typescript", () => {
      expect(getLanguageFromExtension(".ts")).toBe("typescript");
    });

    it("maps .tsx to typescript", () => {
      expect(getLanguageFromExtension(".tsx")).toBe("typescript");
    });

    it("maps .js to javascript", () => {
      expect(getLanguageFromExtension(".js")).toBe("javascript");
    });

    it("maps .py to python", () => {
      expect(getLanguageFromExtension(".py")).toBe("python");
    });

    it("maps .java to java", () => {
      expect(getLanguageFromExtension(".java")).toBe("java");
    });

    it("maps .go to go", () => {
      expect(getLanguageFromExtension(".go")).toBe("go");
    });

    it("maps .rs to rust", () => {
      expect(getLanguageFromExtension(".rs")).toBe("rust");
    });

    it("maps .rb to ruby", () => {
      expect(getLanguageFromExtension(".rb")).toBe("ruby");
    });

    it("maps .cpp to cpp", () => {
      expect(getLanguageFromExtension(".cpp")).toBe("cpp");
    });

    it("maps .cs to csharp", () => {
      expect(getLanguageFromExtension(".cs")).toBe("csharp");
    });

    it("maps .swift to swift", () => {
      expect(getLanguageFromExtension(".swift")).toBe("swift");
    });

    it("maps .sh to shell", () => {
      expect(getLanguageFromExtension(".sh")).toBe("shell");
    });

    it("maps .md to markdown", () => {
      expect(getLanguageFromExtension(".md")).toBe("markdown");
    });

    it("maps .dart to dart", () => {
      expect(getLanguageFromExtension(".dart")).toBe("dart");
    });

    it("maps .lua to lua", () => {
      expect(getLanguageFromExtension(".lua")).toBe("lua");
    });

    it("maps .sql to sql", () => {
      expect(getLanguageFromExtension(".sql")).toBe("sql");
    });

    it("returns plaintext for unknown extensions", () => {
      expect(getLanguageFromExtension(".xyz")).toBe("plaintext");
      expect(getLanguageFromExtension(".abc")).toBe("plaintext");
      expect(getLanguageFromExtension("")).toBe("plaintext");
    });

    it("maps .r and .R to r", () => {
      expect(getLanguageFromExtension(".r")).toBe("r");
      expect(getLanguageFromExtension(".R")).toBe("r");
    });

    it("maps .kt to kotlin", () => {
      expect(getLanguageFromExtension(".kt")).toBe("kotlin");
    });

    it("maps .scala to scala", () => {
      expect(getLanguageFromExtension(".scala")).toBe("scala");
    });

    it("maps .php to php", () => {
      expect(getLanguageFromExtension(".php")).toBe("php");
    });
  });

  describe("parseExtraExtensions", () => {
    it("returns empty set for undefined", () => {
      expect(parseExtraExtensions(undefined).size).toBe(0);
    });

    it("returns empty set for empty string", () => {
      expect(parseExtraExtensions("").size).toBe(0);
    });

    it("returns empty set for whitespace-only", () => {
      expect(parseExtraExtensions("  ").size).toBe(0);
    });

    it("parses comma-separated extensions with dots", () => {
      const result = parseExtraExtensions(".tpl,.blade,.hbs");
      expect(result).toEqual(new Set([".tpl", ".blade", ".hbs"]));
    });

    it("adds leading dots when missing", () => {
      const result = parseExtraExtensions("tpl,blade");
      expect(result).toEqual(new Set([".tpl", ".blade"]));
    });

    it("trims whitespace and lowercases", () => {
      const result = parseExtraExtensions("  .TPL , .Blade  ");
      expect(result).toEqual(new Set([".tpl", ".blade"]));
    });

    it("deduplicates entries", () => {
      const result = parseExtraExtensions(".tpl,.tpl,tpl");
      expect(result.size).toBe(1);
      expect(result.has(".tpl")).toBe(true);
    });
  });

  describe("mergeExtraExtensions", () => {
    it("returns env var default when no tool param", () => {
      // EXTRA_EXTENSIONS env var is not set in test environment, so should be empty
      const result = mergeExtraExtensions();
      expect(result.size).toBe(0);
    });

    it("returns tool param extensions when provided", () => {
      const result = mergeExtraExtensions(".tpl,.blade");
      expect(result).toEqual(new Set([".tpl", ".blade"]));
    });

    it("returns empty for undefined tool param", () => {
      const result = mergeExtraExtensions(undefined);
      expect(result.size).toBe(0);
    });
  });
});

describe("resolveQdrantPort", () => {
  it("returns explicit port from URL", () => {
    expect(resolveQdrantPort("https://qdrant.example.com:6333")).toBe(6333);
    expect(resolveQdrantPort("http://localhost:8080")).toBe(8080);
    expect(resolveQdrantPort("https://my-qdrant.com:8443")).toBe(8443);
  });

  it("defaults to 443 for HTTPS URLs without explicit port", () => {
    expect(resolveQdrantPort("https://qdrant.example.com")).toBe(443);
    expect(resolveQdrantPort("https://my-tunnel.trycloudflare.com")).toBe(443);
  });

  it("defaults to 6333 for HTTP URLs without explicit port", () => {
    expect(resolveQdrantPort("http://localhost")).toBe(6333);
    expect(resolveQdrantPort("http://192.168.1.100")).toBe(6333);
  });

  it("handles URLs with paths and query strings", () => {
    expect(resolveQdrantPort("https://qdrant.example.com:9999/some/path")).toBe(9999);
    expect(resolveQdrantPort("https://qdrant.example.com/some/path")).toBe(443);
  });
});
