/**
 * Tests for model cache.
 */

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { ModelCache } from "../../src/llm/cache";
import { safeRm } from "../helpers/cleanup";

const GGUF_BYTES = new Uint8Array([0x47, 0x47, 0x55, 0x46, 0, 0, 0, 0]);

describe("ModelCache", () => {
  let tempDir: string;
  let cache: ModelCache;

  beforeEach(async () => {
    tempDir = join(
      tmpdir(),
      `gno-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
    );
    await mkdir(tempDir, { recursive: true });
    cache = new ModelCache(tempDir);
  });

  afterEach(async () => {
    await safeRm(tempDir);
  });

  describe("resolve", () => {
    test("returns error for file: URI when file does not exist", async () => {
      const result = await cache.resolve(
        "file:/nonexistent/model.gguf",
        "embed"
      );
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe("MODEL_NOT_FOUND");
      }
    });

    test("returns path for file: URI when file exists", async () => {
      const modelPath = join(tempDir, "test-model.gguf");
      await writeFile(modelPath, GGUF_BYTES);

      const result = await cache.resolve(`file:${modelPath}`, "embed");
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value).toBe(modelPath);
      }
    });

    test("returns path for absolute path when file exists", async () => {
      const modelPath = join(tempDir, "test-model.gguf");
      await writeFile(modelPath, GGUF_BYTES);

      const result = await cache.resolve(modelPath, "embed");
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value).toBe(modelPath);
      }
    });

    test("returns NOT_CACHED for uncached hf: URI", async () => {
      const result = await cache.resolve("hf:test/model/model.gguf", "embed");
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe("MODEL_NOT_CACHED");
      }
    });

    test("rejects invalid user-owned GGUF path without deleting it", async () => {
      const modelPath = join(tempDir, "bad-user-model.gguf");
      await writeFile(modelPath, "not gguf");

      const result = await cache.resolve(`file:${modelPath}`, "embed");

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe("INVALID_MODEL_FILE");
      }
      expect(await Bun.file(modelPath).exists()).toBe(true);
    });

    test("returns error for invalid URI", async () => {
      const result = await cache.resolve("invalid-uri", "embed");
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe("INVALID_URI");
      }
    });
  });

  describe("isCached", () => {
    test("returns false for uncached model", async () => {
      const isCached = await cache.isCached("hf:test/model/model.gguf");
      expect(isCached).toBe(false);
    });

    test("returns true for file: URI when file exists", async () => {
      const modelPath = join(tempDir, "local-model.gguf");
      await writeFile(modelPath, GGUF_BYTES);

      const isCached = await cache.isCached(`file:${modelPath}`);
      expect(isCached).toBe(true);
    });

    test("returns false for file: URI when file does not exist", async () => {
      const isCached = await cache.isCached("file:/nonexistent/model.gguf");
      expect(isCached).toBe(false);
    });

    test("returns true for absolute path when file exists", async () => {
      const modelPath = join(tempDir, "local-model.gguf");
      await writeFile(modelPath, GGUF_BYTES);

      const isCached = await cache.isCached(modelPath);
      expect(isCached).toBe(true);
    });

    test("returns false for stale manifest entry", async () => {
      // Write a manifest with an entry pointing to a non-existent file
      const manifestPath = join(tempDir, "manifest.json");
      await writeFile(
        manifestPath,
        JSON.stringify({
          version: "1.0",
          models: [
            {
              uri: "hf:test/model/model.gguf",
              type: "embed",
              path: "/nonexistent/path.gguf",
              size: 100,
              checksum: "",
              cachedAt: new Date().toISOString(),
            },
          ],
        })
      );

      const isCached = await cache.isCached("hf:test/model/model.gguf");
      expect(isCached).toBe(false);
    });

    test("removes cached HTML model and manifest entry", async () => {
      const modelPath = join(tempDir, "html-model.gguf");
      await writeFile(modelPath, "<html><body>login</body></html>");
      const manifestPath = join(tempDir, "manifest.json");
      await writeFile(
        manifestPath,
        JSON.stringify({
          version: "1.0",
          models: [
            {
              uri: "hf:test/html/model.gguf",
              type: "embed",
              path: modelPath,
              size: 32,
              checksum: "",
              cachedAt: new Date().toISOString(),
            },
          ],
        })
      );

      const result = await cache.resolve("hf:test/html/model.gguf", "embed");

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe("MODEL_DOWNLOAD_INTERCEPTED");
      }
      expect(await Bun.file(modelPath).exists()).toBe(false);
      expect(await cache.list()).toEqual([]);
    });
  });

  describe("list", () => {
    test("returns empty array when no manifest", async () => {
      const entries = await cache.list();
      expect(entries).toEqual([]);
    });

    test("stores SHA-256 checksum when adding a cached model", async () => {
      const modelPath = join(tempDir, "checksummed-model.gguf");
      const modelContent = GGUF_BYTES;
      await writeFile(modelPath, modelContent);

      const cachePrivate = cache as unknown as {
        addToManifest: (
          uri: string,
          type: "embed" | "rerank" | "expand" | "gen",
          path: string
        ) => Promise<void>;
      };
      await cachePrivate.addToManifest(
        "hf:test/model/model.gguf",
        "embed",
        modelPath
      );

      const entries = await cache.list();
      expect(entries).toHaveLength(1);
      expect(entries[0]?.checksum).toBe(
        new Bun.CryptoHasher("sha256").update(modelContent).digest("hex")
      );
    });

    test("returns entries from manifest", async () => {
      const manifestPath = join(tempDir, "manifest.json");
      const modelPath = join(tempDir, "model.gguf");
      await writeFile(modelPath, "test content");

      await writeFile(
        manifestPath,
        JSON.stringify({
          version: "1.0",
          models: [
            {
              uri: "hf:test/model/model.gguf",
              type: "embed",
              path: modelPath,
              size: 12,
              checksum: "",
              cachedAt: "2024-01-01T00:00:00Z",
            },
          ],
        })
      );

      const entries = await cache.list();
      expect(entries).toHaveLength(1);
      expect(entries[0]?.uri).toBe("hf:test/model/model.gguf");
      expect(entries[0]?.type).toBe("embed");
    });
  });

  describe("totalSize", () => {
    test("returns 0 for empty cache", async () => {
      const size = await cache.totalSize();
      expect(size).toBe(0);
    });

    test("returns sum of model sizes", async () => {
      const manifestPath = join(tempDir, "manifest.json");
      await writeFile(
        manifestPath,
        JSON.stringify({
          version: "1.0",
          models: [
            {
              uri: "hf:test/embed/model.gguf",
              type: "embed",
              path: "/path/embed.gguf",
              size: 100,
              checksum: "",
              cachedAt: "2024-01-01T00:00:00Z",
            },
            {
              uri: "hf:test/gen/model.gguf",
              type: "gen",
              path: "/path/gen.gguf",
              size: 200,
              checksum: "",
              cachedAt: "2024-01-01T00:00:00Z",
            },
          ],
        })
      );

      const size = await cache.totalSize();
      expect(size).toBe(300);
    });
  });

  describe("ensureModel", () => {
    test("returns cached path if model exists", async () => {
      const modelPath = join(tempDir, "test-model.gguf");
      await writeFile(modelPath, GGUF_BYTES);

      // file: URIs are always "cached" if file exists
      const result = await cache.ensureModel(`file:${modelPath}`, "embed", {
        offline: false,
        allowDownload: false,
      });

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value).toBe(modelPath);
      }
    });

    test("returns error in offline mode for uncached hf: URI", async () => {
      const result = await cache.ensureModel(
        "hf:test/model/model.gguf",
        "embed",
        {
          offline: true,
          allowDownload: false,
        }
      );

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe("MODEL_NOT_CACHED");
      }
    });

    test("returns error when auto-download disabled for uncached hf: URI", async () => {
      const result = await cache.ensureModel(
        "hf:test/model/model.gguf",
        "embed",
        {
          offline: false,
          allowDownload: false,
        }
      );

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe("AUTO_DOWNLOAD_DISABLED");
      }
    });

    test("returns error for non-existent file: URI", async () => {
      const result = await cache.ensureModel(
        "file:/nonexistent/model.gguf",
        "embed",
        {
          offline: false,
          allowDownload: true,
        }
      );

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe("MODEL_NOT_FOUND");
      }
    });

    test("returns error for invalid URI", async () => {
      const result = await cache.ensureModel("invalid-uri", "embed", {
        offline: false,
        allowDownload: true,
      });

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe("INVALID_URI");
      }
    });
  });

  describe("clear", () => {
    test("clears all models", async () => {
      const manifestPath = join(tempDir, "manifest.json");
      const modelPath = join(tempDir, "model.gguf");
      await writeFile(modelPath, "test content");

      await writeFile(
        manifestPath,
        JSON.stringify({
          version: "1.0",
          models: [
            {
              uri: "hf:test/model/model.gguf",
              type: "embed",
              path: modelPath,
              size: 12,
              checksum: "",
              cachedAt: "2024-01-01T00:00:00Z",
            },
          ],
        })
      );

      await cache.clear();

      const entries = await cache.list();
      expect(entries).toEqual([]);
    });

    test("clears only specified types", async () => {
      const manifestPath = join(tempDir, "manifest.json");
      const embedPath = join(tempDir, "embed.gguf");
      const genPath = join(tempDir, "gen.gguf");
      await writeFile(embedPath, "embed");
      await writeFile(genPath, "gen");

      await writeFile(
        manifestPath,
        JSON.stringify({
          version: "1.0",
          models: [
            {
              uri: "hf:test/embed/model.gguf",
              type: "embed",
              path: embedPath,
              size: 5,
              checksum: "",
              cachedAt: "2024-01-01T00:00:00Z",
            },
            {
              uri: "hf:test/gen/model.gguf",
              type: "gen",
              path: genPath,
              size: 3,
              checksum: "",
              cachedAt: "2024-01-01T00:00:00Z",
            },
          ],
        })
      );

      await cache.clear(["embed"]);

      const entries = await cache.list();
      expect(entries).toHaveLength(1);
      expect(entries[0]?.type).toBe("gen");
    });
  });
});
