import { describe, expect, test } from "@/test";
import { SelectLlmProviderApiKeySchema } from "@/types";
import LlmProviderApiKeyModel from "./llm-provider-api-key";

describe("LlmProviderApiKeyModel", () => {
  describe("create", () => {
    test("can create a personal LLM provider API key", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      const apiKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "My Personal Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });

      expect(apiKey).toBeDefined();
      expect(apiKey.id).toBeDefined();
      expect(apiKey.organizationId).toBe(org.id);
      expect(apiKey.name).toBe("My Personal Key");
      expect(apiKey.provider).toBe("anthropic");
      expect(apiKey.scope).toBe("personal");
      expect(apiKey.userId).toBe(user.id);
      expect(apiKey.teamId).toBeNull();
    });

    test("can create a team LLM provider API key", async ({
      makeOrganization,
      makeUser,
      makeTeam,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const team = await makeTeam(org.id, user.id, { name: "Test Team" });

      const apiKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Team Key",
        provider: "anthropic",
        scope: "team",
        teamId: team.id,
      });

      expect(apiKey.scope).toBe("team");
      expect(apiKey.teamId).toBe(team.id);
      expect(apiKey.userId).toBeNull();
    });

    test("can create an org-wide LLM provider API key", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      const apiKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Org Wide Key",
        provider: "anthropic",
        scope: "org",
      });

      expect(apiKey.scope).toBe("org");
      expect(apiKey.userId).toBeNull();
      expect(apiKey.teamId).toBeNull();
    });

    test("allows multiple keys per provider and scope", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      const key1 = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Personal Key 1",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });

      const key2 = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Personal Key 2",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });

      expect(key1.id).toBeDefined();
      expect(key2.id).toBeDefined();
      expect(key1.id).not.toBe(key2.id);
    });

    test("can create key with isPrimary", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      const key = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Primary Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
        isPrimary: true,
      });

      expect(key.isPrimary).toBe(true);
    });

    test("allows personal keys for different providers", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      const anthropicKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Anthropic Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });

      const openaiKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "OpenAI Key",
        provider: "openai",
        scope: "personal",
        userId: user.id,
      });

      expect(anthropicKey.provider).toBe("anthropic");
      expect(openaiKey.provider).toBe("openai");
    });

    test("baseUrl and inferenceBaseUrl are nullable and round-trip", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      // Key without baseUrl should have null
      const keyWithoutBaseUrl = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "No BaseUrl Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });
      expect(keyWithoutBaseUrl.baseUrl).toBeNull();
      expect(keyWithoutBaseUrl.inferenceBaseUrl).toBeNull();

      // Key with baseUrl should store it
      const keyWithBaseUrl = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Custom BaseUrl Key",
        provider: "openai",
        scope: "personal",
        userId: user.id,
        baseUrl: "https://custom-api.example.com",
        inferenceBaseUrl: "https://runtime-api.example.com",
      });
      expect(keyWithBaseUrl.baseUrl).toBe("https://custom-api.example.com");
      expect(keyWithBaseUrl.inferenceBaseUrl).toBe(
        "https://runtime-api.example.com",
      );

      // Verify via findById that nullable baseUrl round-trips correctly
      const found = await LlmProviderApiKeyModel.findById(keyWithoutBaseUrl.id);
      expect(found?.baseUrl).toBeNull();
      expect(found?.inferenceBaseUrl).toBeNull();
    });
  });

  describe("findById", () => {
    test("can find an LLM provider API key by ID", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const created = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Test Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });

      const found = await LlmProviderApiKeyModel.findById(created.id);

      expect(found).toBeDefined();
      expect(found?.id).toBe(created.id);
      expect(found?.name).toBe("Test Key");
    });

    test("returns null for non-existent ID", async () => {
      const found = await LlmProviderApiKeyModel.findById(
        "00000000-0000-0000-0000-000000000000",
      );
      expect(found).toBeNull();
    });
  });

  describe("findByOrganizationId", () => {
    test("can find all LLM provider API keys for an organization", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Key 1",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });
      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Key 2",
        provider: "openai",
        scope: "org",
      });

      const keys = await LlmProviderApiKeyModel.findByOrganizationId(org.id);

      expect(keys).toHaveLength(2);
      expect(keys.map((k) => k.name)).toContain("Key 1");
      expect(keys.map((k) => k.name)).toContain("Key 2");
    });
  });

  describe("findByScope", () => {
    test("can find org-wide key by scope", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      const orgWideKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Org Wide Key",
        provider: "anthropic",
        scope: "org",
      });

      const found = await LlmProviderApiKeyModel.findByScope(
        org.id,
        "anthropic",
        "org",
      );

      expect(found).toBeDefined();
      expect(found?.id).toBe(orgWideKey.id);
    });

    test("returns null when no key exists for scope", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      const found = await LlmProviderApiKeyModel.findByScope(
        org.id,
        "anthropic",
        "org",
      );

      expect(found).toBeNull();
    });
  });

  describe("update", () => {
    test("can update a chat API key", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const apiKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Original Name",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });

      const updated = await LlmProviderApiKeyModel.update(apiKey.id, {
        name: "Updated Name",
      });

      expect(updated).toBeDefined();
      expect(updated?.name).toBe("Updated Name");
    });
  });

  describe("delete", () => {
    test("can delete a chat API key", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const apiKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "To Delete",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });

      const deleted = await LlmProviderApiKeyModel.delete(apiKey.id);
      const found = await LlmProviderApiKeyModel.findById(apiKey.id);

      expect(deleted).toBe(true);
      expect(found).toBeNull();
    });
  });

  describe("getVisibleKeys", () => {
    test("user sees their own personal keys", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user1 = await makeUser({ email: "user1@test.com" });
      const user2 = await makeUser({ email: "user2@test.com" });

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "User1 Personal Key",
        provider: "anthropic",
        scope: "personal",
        userId: user1.id,
      });
      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "User2 Personal Key",
        provider: "anthropic",
        scope: "personal",
        userId: user2.id,
      });

      const visibleToUser1 = await LlmProviderApiKeyModel.getVisibleKeys(
        org.id,
        user1.id,
        [],
        false,
      );

      expect(visibleToUser1).toHaveLength(1);
      expect(visibleToUser1[0].name).toBe("User1 Personal Key");
    });

    test("user sees team keys for their teams", async ({
      makeOrganization,
      makeUser,
      makeTeam,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const team = await makeTeam(org.id, user.id, { name: "Test Team" });

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Team Key",
        provider: "anthropic",
        scope: "team",
        teamId: team.id,
      });

      const visible = await LlmProviderApiKeyModel.getVisibleKeys(
        org.id,
        user.id,
        [team.id],
        false,
      );

      expect(visible).toHaveLength(1);
      expect(visible[0].name).toBe("Team Key");
    });

    test("user sees org-wide keys", async ({ makeOrganization, makeUser }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Org Wide Key",
        provider: "anthropic",
        scope: "org",
      });

      const visible = await LlmProviderApiKeyModel.getVisibleKeys(
        org.id,
        user.id,
        [],
        false,
      );

      expect(visible).toHaveLength(1);
      expect(visible[0].name).toBe("Org Wide Key");
    });

    test("admin sees all keys except other users personal keys", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const admin = await makeUser({ email: "admin@test.com" });
      const user = await makeUser({ email: "user@test.com" });

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Admin Personal Key",
        provider: "anthropic",
        scope: "personal",
        userId: admin.id,
      });
      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "User Personal Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });
      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Org Wide Key",
        provider: "openai",
        scope: "org",
      });

      const visible = await LlmProviderApiKeyModel.getVisibleKeys(
        org.id,
        admin.id,
        [],
        true, // isAgentAdmin
      );

      // Admin sees own personal key, all team keys, all org-wide keys, but not other users' personal keys
      expect(visible).toHaveLength(2);
      expect(visible.map((k) => k.name)).toContain("Admin Personal Key");
      expect(visible.map((k) => k.name)).toContain("Org Wide Key");
      expect(visible.map((k) => k.name)).not.toContain("User Personal Key");
    });

    test("supports filtering visible keys by search and provider", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Primary Anthropic Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });
      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "OpenAI Backup",
        provider: "openai",
        scope: "personal",
        userId: user.id,
      });

      const visible = await LlmProviderApiKeyModel.getVisibleKeys(
        org.id,
        user.id,
        [],
        false,
        {
          search: "primary",
          provider: "anthropic",
        },
      );

      expect(visible).toHaveLength(1);
      expect(visible[0].name).toBe("Primary Anthropic Key");
    });

    test("treats LIKE wildcard characters in search as literals", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Primary%Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });
      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Primary Alpha Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });

      const visible = await LlmProviderApiKeyModel.getVisibleKeys(
        org.id,
        user.id,
        [],
        false,
        {
          search: "%",
        },
      );

      expect(visible).toHaveLength(1);
      expect(visible[0].name).toBe("Primary%Key");
    });
  });

  describe("resolveApiKey", () => {
    test("returns personal key first", async ({
      makeOrganization,
      makeUser,
      makeSecret,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const secret1 = await makeSecret();
      const secret2 = await makeSecret();

      const personalKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Personal Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
        secretId: secret1.id,
      });
      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Org Wide Key",
        provider: "anthropic",
        scope: "org",
        secretId: secret2.id,
      });

      const resolved = await LlmProviderApiKeyModel.getCurrentApiKey({
        organizationId: org.id,
        userId: user.id,
        userTeamIds: [],
        provider: "anthropic",
        conversationId: null,
      });

      expect(resolved?.id).toBe(personalKey.id);
    });

    test("falls back to team key when no personal key", async ({
      makeOrganization,
      makeUser,
      makeTeam,
      makeSecret,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const team = await makeTeam(org.id, user.id, { name: "Test Team" });
      const secret1 = await makeSecret();
      const secret2 = await makeSecret();

      const teamKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Team Key",
        provider: "anthropic",
        scope: "team",
        teamId: team.id,
        secretId: secret1.id,
      });
      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Org Wide Key",
        provider: "anthropic",
        scope: "org",
        secretId: secret2.id,
      });

      const resolved = await LlmProviderApiKeyModel.getCurrentApiKey({
        organizationId: org.id,
        userId: user.id,
        userTeamIds: [team.id],
        provider: "anthropic",
        conversationId: null,
      });

      expect(resolved?.id).toBe(teamKey.id);
    });

    test("falls back to org-wide key when no personal or team key", async ({
      makeOrganization,
      makeUser,
      makeSecret,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const secret = await makeSecret();

      const orgWideKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Org Wide Key",
        provider: "anthropic",
        scope: "org",
        secretId: secret.id,
      });

      const resolved = await LlmProviderApiKeyModel.getCurrentApiKey({
        organizationId: org.id,
        userId: user.id,
        userTeamIds: [],
        provider: "anthropic",
        conversationId: null,
      });

      expect(resolved?.id).toBe(orgWideKey.id);
    });

    test("returns conversation key when specified", async ({
      makeOrganization,
      makeUser,
      makeSecret,
      makeAgent,
      makeConversation,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const secret1 = await makeSecret();
      const secret2 = await makeSecret();
      const agent = await makeAgent({ name: "Test Agent", teams: [] });

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Personal Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
        secretId: secret1.id,
      });
      const conversationKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Org Wide Key",
        provider: "anthropic",
        scope: "org",
        secretId: secret2.id,
      });

      // Create a conversation with the org-wide key as its chatApiKeyId
      const conversation = await makeConversation(agent.id, {
        userId: user.id,
        organizationId: org.id,
        chatApiKeyId: conversationKey.id,
      });

      const resolved = await LlmProviderApiKeyModel.getCurrentApiKey({
        organizationId: org.id,
        userId: user.id,
        userTeamIds: [],
        provider: "anthropic",
        conversationId: conversation.id,
      });

      expect(resolved?.id).toBe(conversationKey.id);
    });

    test("returns null when no keys available", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      const resolved = await LlmProviderApiKeyModel.getCurrentApiKey({
        organizationId: org.id,
        userId: user.id,
        userTeamIds: [],
        provider: "anthropic",
        conversationId: null,
      });

      expect(resolved).toBeNull();
    });

    test("prefers isPrimary key over older key in same scope", async ({
      makeOrganization,
      makeUser,
      makeSecret,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const secret1 = await makeSecret();
      const secret2 = await makeSecret();

      // Create an older key (not primary)
      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Older Key",
        provider: "anthropic",
        scope: "org",
        secretId: secret1.id,
        isPrimary: false,
      });

      // Create a newer key marked as primary
      const primaryKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Primary Key",
        provider: "anthropic",
        scope: "org",
        secretId: secret2.id,
        isPrimary: true,
      });

      const resolved = await LlmProviderApiKeyModel.getCurrentApiKey({
        organizationId: org.id,
        userId: user.id,
        userTeamIds: [],
        provider: "anthropic",
        conversationId: null,
      });

      expect(resolved?.id).toBe(primaryKey.id);
    });

    test("falls back to oldest key when no primary is set", async ({
      makeOrganization,
      makeUser,
      makeSecret,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      const secret1 = await makeSecret();
      const secret2 = await makeSecret();

      // Create two keys, neither is primary — oldest should win
      const olderKey = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Older Key",
        provider: "anthropic",
        scope: "org",
        secretId: secret1.id,
      });

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Newer Key",
        provider: "anthropic",
        scope: "org",
        secretId: secret2.id,
      });

      const resolved = await LlmProviderApiKeyModel.getCurrentApiKey({
        organizationId: org.id,
        userId: user.id,
        userTeamIds: [],
        provider: "anthropic",
        conversationId: null,
      });

      expect(resolved?.id).toBe(olderKey.id);
    });
  });

  describe("hasAnyApiKey", () => {
    test("returns true when organization has API keys", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Test Key",
        provider: "anthropic",
        scope: "org",
      });

      const hasKeys = await LlmProviderApiKeyModel.hasAnyApiKey(org.id);

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

    test("returns false when organization has no API keys", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      const hasKeys = await LlmProviderApiKeyModel.hasAnyApiKey(org.id);

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

  describe("hasConfiguredApiKey", () => {
    test("returns true when configured API key exists for provider", async ({
      makeOrganization,
      makeSecret,
    }) => {
      const org = await makeOrganization();
      const secret = await makeSecret();

      await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Anthropic Key",
        provider: "anthropic",
        scope: "org",
        secretId: secret.id,
      });

      const hasAnthropic = await LlmProviderApiKeyModel.hasConfiguredApiKey(
        org.id,
        "anthropic",
      );
      const hasOpenai = await LlmProviderApiKeyModel.hasConfiguredApiKey(
        org.id,
        "openai",
      );

      expect(hasAnthropic).toBe(true);
      expect(hasOpenai).toBe(false);
    });
  });

  describe("SelectLlmProviderApiKeySchema", () => {
    test("accepts null baseUrl without validation error", async ({
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();

      const key = await LlmProviderApiKeyModel.create({
        organizationId: org.id,
        name: "Test Key",
        provider: "anthropic",
        scope: "personal",
        userId: user.id,
      });

      // This would throw if baseUrl is not marked as nullable in the schema
      const result = SelectLlmProviderApiKeySchema.safeParse(key);
      expect(result.success).toBe(true);
    });
  });
});
