import { describe, expect, test } from "@/test";
import LlmProviderApiKeyModelLinkModel from "./llm-provider-api-key-model";
import ModelModel from "./model";

describe("LlmProviderApiKeyModelLinkModel", () => {
  describe("getBestModelsForApiKeys", () => {
    test("returns an empty map for empty input", async () => {
      const bestModels =
        await LlmProviderApiKeyModelLinkModel.getBestModelsForApiKeys([]);

      expect(bestModels).toEqual(new Map());
    });

    test("returns best-marked models and falls back to the first linked model", async ({
      makeOrganization,
      makeSecret,
      makeLlmProviderApiKey,
    }) => {
      const org = await makeOrganization();
      const secret = await makeSecret();

      const bestMarkedKey = await makeLlmProviderApiKey(org.id, secret.id, {
        provider: "openai",
      });
      const fallbackKey = await makeLlmProviderApiKey(org.id, secret.id, {
        provider: "openai",
      });

      const fallbackFirstModel = await ModelModel.create({
        externalId: "openai/gpt-5.4-mini",
        provider: "openai",
        modelId: "gpt-5.4-mini",
        description: "GPT-5.4 Mini",
        contextLength: 128000,
        inputModalities: ["text"],
        outputModalities: ["text"],
        supportsToolCalling: true,
        promptPricePerToken: "0.000001",
        completionPricePerToken: "0.000002",
        lastSyncedAt: new Date(),
      });
      const fallbackSecondModel = await ModelModel.create({
        externalId: "openai/gpt-5.4",
        provider: "openai",
        modelId: "gpt-5.4",
        description: "GPT-5.4",
        contextLength: 200000,
        inputModalities: ["text"],
        outputModalities: ["text"],
        supportsToolCalling: true,
        promptPricePerToken: "0.000002",
        completionPricePerToken: "0.000006",
        lastSyncedAt: new Date(),
      });
      const bestCandidateModel = await ModelModel.create({
        externalId: "openai/gpt-5.5",
        provider: "openai",
        modelId: "gpt-5.5",
        description: "GPT-5.5",
        contextLength: 128000,
        inputModalities: ["text"],
        outputModalities: ["text"],
        supportsToolCalling: true,
        promptPricePerToken: "0.000002",
        completionPricePerToken: "0.000008",
        lastSyncedAt: new Date(),
      });

      await LlmProviderApiKeyModelLinkModel.syncModelsForApiKey(
        bestMarkedKey.id,
        [
          { id: fallbackFirstModel.id, modelId: fallbackFirstModel.modelId },
          { id: bestCandidateModel.id, modelId: bestCandidateModel.modelId },
        ],
        "openai",
      );
      await LlmProviderApiKeyModelLinkModel.linkModelsToApiKey(fallbackKey.id, [
        fallbackSecondModel.id,
        fallbackFirstModel.id,
      ]);

      const bestModels =
        await LlmProviderApiKeyModelLinkModel.getBestModelsForApiKeys([
          bestMarkedKey.id,
          fallbackKey.id,
        ]);

      expect(bestModels.get(bestMarkedKey.id)?.id).toBe(bestCandidateModel.id);
      expect(bestModels.get(fallbackKey.id)?.id).toBe(fallbackSecondModel.id);
    });
  });

  describe("getAllModelsWithApiKeys", () => {
    test("returns empty array when no models exist", async ({
      makeOrganization,
    }) => {
      await makeOrganization();
      const result =
        await LlmProviderApiKeyModelLinkModel.getAllModelsWithApiKeys();
      expect(result).toEqual([]);
    });

    test("returns models that have linked API keys", async ({
      makeOrganization,
      makeSecret,
      makeLlmProviderApiKey,
    }) => {
      const org = await makeOrganization();
      const secret = await makeSecret();

      // Create an API key
      const apiKey = await makeLlmProviderApiKey(org.id, secret.id, {
        provider: "openai",
      });

      // Create a model and link it
      const model = await ModelModel.create({
        externalId: "openai/gpt-5.5",
        provider: "openai",
        modelId: "gpt-5.5",
        description: "GPT-5.5",
        contextLength: 128000,
        inputModalities: ["text"],
        outputModalities: ["text"],
        supportsToolCalling: true,
        promptPricePerToken: "0.000005",
        completionPricePerToken: "0.000015",
        lastSyncedAt: new Date(),
      });

      await LlmProviderApiKeyModelLinkModel.linkModelsToApiKey(apiKey.id, [
        model.id,
      ]);

      const result =
        await LlmProviderApiKeyModelLinkModel.getAllModelsWithApiKeys();
      expect(result).toHaveLength(1);
      expect(result[0].model.id).toBe(model.id);
      expect(result[0].apiKeys).toHaveLength(1);
      expect(result[0].apiKeys[0].id).toBe(apiKey.id);
    });

    test("excludes orphaned models with no linked API keys", async ({
      makeOrganization,
      makeSecret,
      makeLlmProviderApiKey,
    }) => {
      const org = await makeOrganization();
      const secret = await makeSecret();

      // Create an API key and a linked model
      const apiKey = await makeLlmProviderApiKey(org.id, secret.id, {
        provider: "openai",
      });
      const linkedModel = await ModelModel.create({
        externalId: "openai/gpt-5.5",
        provider: "openai",
        modelId: "gpt-5.5",
        description: "GPT-5.5",
        contextLength: 128000,
        inputModalities: ["text"],
        outputModalities: ["text"],
        supportsToolCalling: true,
        promptPricePerToken: "0.000005",
        completionPricePerToken: "0.000015",
        lastSyncedAt: new Date(),
      });
      await LlmProviderApiKeyModelLinkModel.linkModelsToApiKey(apiKey.id, [
        linkedModel.id,
      ]);

      // Create an orphaned model (no API key link)
      await ModelModel.create({
        externalId: "openai/gpt-5.4-mini",
        provider: "openai",
        modelId: "gpt-5.4-mini",
        description: "GPT-5.4 Mini",
        contextLength: 16000,
        inputModalities: ["text"],
        outputModalities: ["text"],
        supportsToolCalling: true,
        promptPricePerToken: "0.000001",
        completionPricePerToken: "0.000002",
        lastSyncedAt: new Date(),
      });

      const result =
        await LlmProviderApiKeyModelLinkModel.getAllModelsWithApiKeys();

      // Only the linked model should be returned
      expect(result).toHaveLength(1);
      expect(result[0].model.id).toBe(linkedModel.id);
      expect(result[0].model.modelId).toBe("gpt-5.5");
    });

    test("orphaned models appear after API key deletion due to cascade", async ({
      makeOrganization,
      makeSecret,
      makeLlmProviderApiKey,
    }) => {
      const org = await makeOrganization();
      const secret = await makeSecret();

      // Create API key and link a model
      const apiKey = await makeLlmProviderApiKey(org.id, secret.id, {
        provider: "anthropic",
      });
      const model = await ModelModel.create({
        externalId: "anthropic/claude-opus-4-7",
        provider: "anthropic",
        modelId: "claude-opus-4-7",
        description: "Claude Opus 4.7",
        contextLength: 200000,
        inputModalities: ["text"],
        outputModalities: ["text"],
        supportsToolCalling: true,
        promptPricePerToken: "0.000003",
        completionPricePerToken: "0.000015",
        lastSyncedAt: new Date(),
      });
      await LlmProviderApiKeyModelLinkModel.linkModelsToApiKey(apiKey.id, [
        model.id,
      ]);

      // Verify model is visible before deletion
      let result =
        await LlmProviderApiKeyModelLinkModel.getAllModelsWithApiKeys();
      expect(result).toHaveLength(1);

      // Delete the API key (cascade deletes api_key_models entries)
      const { LlmProviderApiKeyModel } = await import("@/models");
      await LlmProviderApiKeyModel.delete(apiKey.id);

      // Model should no longer appear since it has no linked API keys
      result = await LlmProviderApiKeyModelLinkModel.getAllModelsWithApiKeys();
      expect(result).toHaveLength(0);

      // But the model itself still exists in the models table
      const orphanedModel = await ModelModel.findById(model.id);
      expect(orphanedModel).not.toBeNull();
    });
  });

  describe("syncModelsForApiKey", () => {
    test("deduplicates repeated models before inserting links", async ({
      makeOrganization,
      makeSecret,
      makeLlmProviderApiKey,
    }) => {
      const org = await makeOrganization();
      const secret = await makeSecret();
      const apiKey = await makeLlmProviderApiKey(org.id, secret.id, {
        provider: "openai",
      });

      const model = await ModelModel.create({
        externalId: "openai/gpt-5.5",
        provider: "openai",
        modelId: "gpt-5.5",
        description: "GPT-5.5",
        contextLength: 128000,
        inputModalities: ["text"],
        outputModalities: ["text"],
        supportsToolCalling: true,
        promptPricePerToken: "0.000002",
        completionPricePerToken: "0.000008",
        lastSyncedAt: new Date(),
      });

      await LlmProviderApiKeyModelLinkModel.syncModelsForApiKey(
        apiKey.id,
        [
          { id: model.id, modelId: model.modelId },
          { id: model.id, modelId: model.modelId },
        ],
        "openai",
      );

      const linkedModels =
        await LlmProviderApiKeyModelLinkModel.getModelsForApiKey(apiKey.id);

      expect(linkedModels).toHaveLength(1);
      expect(linkedModels[0].id).toBe(model.id);
    });
  });
});
