import { DEFAULT_THEME_ID } from "@archestra/shared";
import { eq } from "drizzle-orm";
import db, { schema } from "@/database";
import { describe, expect, test } from "@/test";
import { UpdateAppearanceSettingsSchema } from "@/types";
import OrganizationModel from "./organization";

// Minimal valid 1x1 transparent PNG (Base64-encoded)
const VALID_PNG_BASE64 =
  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/58BAwAI/AL+hc2rNAAAAABJRU5ErkJggg==";

describe("OrganizationModel", () => {
  describe("getAppearanceSettings", () => {
    test("should return default appearance when no organization exists", async () => {
      // Ensure no organizations exist (test setup clears DB)
      const appearance = await OrganizationModel.getAppearanceSettings();

      expect(appearance).toEqual({
        theme: DEFAULT_THEME_ID,
        customFont: "lato",
        logo: null,
        logoDark: null,
        favicon: null,
        iconLogo: null,
        iconLogoDark: null,
        appName: null,
        ogDescription: null,
        footerText: null,
        chatLinks: null,
        onboardingWizard: null,
        chatErrorSupportMessage: null,
        slimChatErrorUi: false,
        animateChatPlaceholders: true,
      });
    });

    test("should return organization appearance settings", async ({
      makeOrganization,
    }) => {
      await makeOrganization();

      const appearance = await OrganizationModel.getAppearanceSettings();

      expect(appearance).toEqual({
        theme: "cosmic-night",
        customFont: "lato",
        logo: null,
        logoDark: null,
        favicon: null,
        iconLogo: null,
        iconLogoDark: null,
        appName: null,
        ogDescription: null,
        footerText: null,
        chatLinks: null,
        onboardingWizard: null,
        chatErrorSupportMessage: null,
        slimChatErrorUi: false,
        animateChatPlaceholders: true,
      });
    });

    test("should return custom theme when set", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      // Update organization with custom theme
      await db
        .update(schema.organizationsTable)
        .set({ theme: "twitter" })
        .where(eq(schema.organizationsTable.id, org.id));

      const appearance = await OrganizationModel.getAppearanceSettings();

      expect(appearance.theme).toBe("twitter");
    });

    test("should return custom font when set", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      // Update organization with custom font
      await db
        .update(schema.organizationsTable)
        .set({ customFont: "inter" })
        .where(eq(schema.organizationsTable.id, org.id));

      const appearance = await OrganizationModel.getAppearanceSettings();

      expect(appearance.customFont).toBe("inter");
    });

    test("should return logo when set", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      // Update organization with logo
      await db
        .update(schema.organizationsTable)
        .set({ logo: VALID_PNG_BASE64 })
        .where(eq(schema.organizationsTable.id, org.id));

      const appearance = await OrganizationModel.getAppearanceSettings();

      expect(appearance.logo).toBe(VALID_PNG_BASE64);
    });

    test("should return logoDark when set", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      await db
        .update(schema.organizationsTable)
        .set({ logoDark: VALID_PNG_BASE64 })
        .where(eq(schema.organizationsTable.id, org.id));

      const appearance = await OrganizationModel.getAppearanceSettings();

      expect(appearance.logoDark).toBe(VALID_PNG_BASE64);
    });

    test("should return first organization's appearance when multiple exist", async ({
      makeOrganization,
    }) => {
      // Create first organization with custom settings
      const firstOrg = await makeOrganization();
      await db
        .update(schema.organizationsTable)
        .set({ theme: "claude", customFont: "roboto" })
        .where(eq(schema.organizationsTable.id, firstOrg.id));

      // Create second organization with different settings
      await makeOrganization();

      const appearance = await OrganizationModel.getAppearanceSettings();

      // Should return first organization's appearance
      expect(appearance.theme).toBe("claude");
      expect(appearance.customFont).toBe("roboto");
    });

    test("should only return expected public appearance fields", async ({
      makeOrganization,
    }) => {
      await makeOrganization();

      const appearance = await OrganizationModel.getAppearanceSettings();

      // Verify only expected fields are returned
      expect(Object.keys(appearance).sort()).toEqual([
        "animateChatPlaceholders",
        "appName",
        "chatErrorSupportMessage",
        "chatLinks",
        "customFont",
        "favicon",
        "footerText",
        "iconLogo",
        "iconLogoDark",
        "logo",
        "logoDark",
        "ogDescription",
        "onboardingWizard",
        "slimChatErrorUi",
        "theme",
      ]);
    });

    test("should return animateChatPlaceholders when set", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      await db
        .update(schema.organizationsTable)
        .set({ animateChatPlaceholders: false })
        .where(eq(schema.organizationsTable.id, org.id));

      const appearance = await OrganizationModel.getAppearanceSettings();

      expect(appearance.animateChatPlaceholders).toBe(false);
    });
  });

  describe("getOrCreateDefaultOrganization", () => {
    test("should create default organization when none exists", async () => {
      const org = await OrganizationModel.getOrCreateDefaultOrganization();

      expect(org).toBeDefined();
      expect(org.id).toBe("default-org");
      expect(org.name).toBe("Default Organization");
      expect(org.slug).toBe("default");
    });

    test("should return existing organization when one exists", async ({
      makeOrganization,
    }) => {
      const existingOrg = await makeOrganization();

      const org = await OrganizationModel.getOrCreateDefaultOrganization();

      expect(org.id).toBe(existingOrg.id);
    });
  });

  describe("patch", () => {
    test("should persist the default OAuth token lifetime", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      const found = await OrganizationModel.getById(org.id);

      expect(found?.oauthAccessTokenLifetimeSeconds).toBe(31_536_000);
    });

    test("should update organization theme", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {
        theme: "twitter",
      });

      expect(updated?.theme).toBe("twitter");
    });

    test("should update organization font", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {
        customFont: "inter",
      });

      expect(updated?.customFont).toBe("inter");
    });

    test("should update OAuth token lifetime", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {
        oauthAccessTokenLifetimeSeconds: 604_800,
      });

      expect(updated?.oauthAccessTokenLifetimeSeconds).toBe(604_800);
    });

    test("should accept valid PNG logo", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {
        logo: VALID_PNG_BASE64,
      });

      expect(updated?.logo).toBe(VALID_PNG_BASE64);
    });

    test("should accept null logo (removal)", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      // First set a logo
      await OrganizationModel.patch(org.id, { logo: VALID_PNG_BASE64 });

      // Then remove it
      const updated = await OrganizationModel.patch(org.id, { logo: null });

      expect(updated?.logo).toBeNull();
    });

    test("should accept valid PNG logoDark", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {
        logoDark: VALID_PNG_BASE64,
      });

      expect(updated?.logoDark).toBe(VALID_PNG_BASE64);
    });

    test("should accept null logoDark (removal)", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      await OrganizationModel.patch(org.id, { logoDark: VALID_PNG_BASE64 });
      const updated = await OrganizationModel.patch(org.id, {
        logoDark: null,
      });

      expect(updated?.logoDark).toBeNull();
    });

    test("should return null for non-existent organization", async () => {
      const updated = await OrganizationModel.patch("non-existent-id", {
        theme: "twitter",
      });

      expect(updated).toBeNull();
    });

    test("should return unchanged organization when data is empty", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      // Patch with empty object should not throw "No values to set"
      const updated = await OrganizationModel.patch(org.id, {});

      expect(updated).not.toBeNull();
      expect(updated?.id).toBe(org.id);
      expect(updated?.theme).toBe(org.theme);
    });

    test("should return null when patching non-existent org with empty data", async () => {
      const updated = await OrganizationModel.patch("non-existent-id", {});

      expect(updated).toBeNull();
    });

    test("should update animateChatPlaceholders", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {
        animateChatPlaceholders: false,
      });

      expect(updated?.animateChatPlaceholders).toBe(false);
    });

    test("should update chatLinks", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {
        chatLinks: [
          {
            label: "Docs",
            url: "https://support.example.com/help",
          },
          {
            label: "Status",
            url: "https://status.example.com",
          },
        ],
      });

      expect(updated?.chatLinks).toEqual([
        {
          label: "Docs",
          url: "https://support.example.com/help",
        },
        {
          label: "Status",
          url: "https://status.example.com",
        },
      ]);
    });

    test("should update chatErrorSupportMessage", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {
        chatErrorSupportMessage: "Contact support@example.com for help.",
      });

      expect(updated?.chatErrorSupportMessage).toBe(
        "Contact support@example.com for help.",
      );
    });

    test("should update slimChatErrorUi", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {
        slimChatErrorUi: true,
      });

      expect(updated?.slimChatErrorUi).toBe(true);
    });

    test("should set default LLM model and provider", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      const updated = await OrganizationModel.patch(org.id, {});

      expect(updated).not.toBeNull();
    });

    test("should set default agent ID", async ({
      makeOrganization,
      makeAgent,
    }) => {
      const org = await makeOrganization();
      const agent = await makeAgent({ organizationId: org.id });

      const updated = await OrganizationModel.patch(org.id, {
        defaultAgentId: agent.id,
      });

      expect(updated).not.toBeNull();
      expect(updated?.defaultAgentId).toBe(agent.id);
    });

    test("should clear default agent ID with null", async ({
      makeOrganization,
      makeAgent,
    }) => {
      const org = await makeOrganization();
      const agent = await makeAgent({ organizationId: org.id });

      await OrganizationModel.patch(org.id, { defaultAgentId: agent.id });

      const updated = await OrganizationModel.patch(org.id, {
        defaultAgentId: null,
      });

      expect(updated).not.toBeNull();
      expect(updated?.defaultAgentId).toBeNull();
    });

    test("should update all agent settings at once", async ({
      makeOrganization,
      makeAgent,
    }) => {
      const org = await makeOrganization();
      const agent = await makeAgent({ organizationId: org.id });

      const updated = await OrganizationModel.patch(org.id, {
        defaultAgentId: agent.id,
      });

      expect(updated).not.toBeNull();
      expect(updated?.defaultAgentId).toBe(agent.id);
    });
  });

  describe("patch logoDark validation (via UpdateAppearanceSettingsSchema)", () => {
    const parseLogoDarkField = (logoDark: string | null) =>
      UpdateAppearanceSettingsSchema.shape.logoDark.safeParse(logoDark);

    test("should accept null", () => {
      const result = parseLogoDarkField(null);
      expect(result.success).toBe(true);
    });

    test("should accept valid PNG data URI", () => {
      const result = parseLogoDarkField(VALID_PNG_BASE64);
      expect(result.success).toBe(true);
    });

    test("should reject non-PNG data URI prefix", () => {
      const result = parseLogoDarkField(
        "data:image/jpeg;base64,/9j/4AAQSkZJRg==",
      );
      expect(result.success).toBe(false);
    });
  });

  describe("patch logo validation (via UpdateAppearanceSettingsSchema)", () => {
    const parseLogoField = (logo: string | null) =>
      UpdateAppearanceSettingsSchema.shape.logo.safeParse(logo);

    describe("MIME type validation", () => {
      test("should reject non-PNG data URI prefix", () => {
        const result = parseLogoField(
          "data:image/jpeg;base64,/9j/4AAQSkZJRg==",
        );

        expect(result.success).toBe(false);
        if (!result.success) {
          expect(result.error.issues).toHaveLength(1);
          expect(result.error.issues[0].message).toContain("PNG");
        }
      });

      test("should reject WebP data URI prefix", () => {
        const result = parseLogoField(
          "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAQAcJaQAA3AA/v3AgAA",
        );

        expect(result.success).toBe(false);
        if (!result.success) {
          expect(result.error.issues).toHaveLength(1);
          expect(result.error.issues[0].message).toContain("PNG");
        }
      });
    });

    describe("Base64 validation", () => {
      test("should reject invalid Base64 payload", () => {
        const result = parseLogoField(
          "data:image/png;base64,NotAnImageJustText",
        );

        expect(result.success).toBe(false);
        if (!result.success) {
          expect(result.error.issues).toHaveLength(1);
          expect(result.error.issues[0].message).toContain("Base64");
        }
      });

      test("should reject valid Base64 but non-PNG content", () => {
        // "Hello World" encoded as Base64 — valid Base64 but not a PNG
        const result = parseLogoField("data:image/png;base64,SGVsbG8gV29ybGQ=");

        expect(result.success).toBe(false);
        if (!result.success) {
          expect(result.error.issues).toHaveLength(1);
          expect(result.error.issues[0].message).toContain("PNG image data");
        }
      });
    });

    describe("Format validation", () => {
      test("should reject plain text without data URI prefix", () => {
        const result = parseLogoField("just-a-random-string");

        expect(result.success).toBe(false);
        if (!result.success) {
          expect(result.error.issues).toHaveLength(1);
          expect(result.error.issues[0].message).toContain("data URI");
        }
      });

      test("should reject empty string", () => {
        const result = parseLogoField("");

        expect(result.success).toBe(false);
        if (!result.success) {
          expect(result.error.issues).toHaveLength(1);
          expect(result.error.issues[0].message).toContain("data URI");
        }
      });

      test("should reject malformed data URI", () => {
        const result = parseLogoField("data:image/png;");

        expect(result.success).toBe(false);
        if (!result.success) {
          expect(result.error.issues).toHaveLength(1);
        }
      });
    });

    describe("Valid inputs", () => {
      test("should accept null", () => {
        const result = parseLogoField(null);

        expect(result.success).toBe(true);
        if (result.success) {
          expect(result.data).toBeNull();
        }
      });

      test("should accept valid PNG data URI", () => {
        const result = parseLogoField(VALID_PNG_BASE64);

        expect(result.success).toBe(true);
        if (result.success) {
          expect(result.data).toBe(VALID_PNG_BASE64);
        }
      });
    });
  });

  describe("chatLinks validation (via UpdateAppearanceSettingsSchema)", () => {
    const parseChatLinksField = (
      chatLinks:
        | {
            label: string;
            url: string;
          }[]
        | null,
    ) => UpdateAppearanceSettingsSchema.shape.chatLinks.safeParse(chatLinks);

    test("should accept null", () => {
      const result = parseChatLinksField(null);

      expect(result.success).toBe(true);
    });

    test("should accept up to 3 valid links", () => {
      const result = parseChatLinksField([
        {
          label: "Docs",
          url: "https://docs.example.com",
        },
        {
          label: "Status",
          url: "https://status.example.com",
        },
        {
          label: "Support",
          url: "https://support.example.com",
        },
      ]);

      expect(result.success).toBe(true);
    });

    test("should reject invalid URLs", () => {
      const result = parseChatLinksField([
        {
          label: "Docs",
          url: "not-a-url",
        },
      ]);

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.issues[0]?.message).toContain(
          "valid HTTP or HTTPS URL",
        );
      }
    });

    test("should reject labels longer than 25 characters", () => {
      const result = parseChatLinksField([
        {
          label: "A".repeat(26),
          url: "https://docs.example.com",
        },
      ]);

      expect(result.success).toBe(false);
    });

    test("should reject more than 3 links", () => {
      const result = parseChatLinksField([
        {
          label: "One",
          url: "https://one.example.com",
        },
        {
          label: "Two",
          url: "https://two.example.com",
        },
        {
          label: "Three",
          url: "https://three.example.com",
        },
        {
          label: "Four",
          url: "https://four.example.com",
        },
      ]);

      expect(result.success).toBe(false);
    });
  });

  describe("getById", () => {
    test("should return organization by id", async ({ makeOrganization }) => {
      const org = await makeOrganization();

      const found = await OrganizationModel.getById(org.id);

      expect(found?.id).toBe(org.id);
      expect(found?.name).toBe(org.name);
    });

    test("should return null for non-existent id", async () => {
      const found = await OrganizationModel.getById("non-existent-id");

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

    test("should return defaultAgentId after patch", async ({
      makeOrganization,
      makeAgent,
    }) => {
      const org = await makeOrganization();
      const agent = await makeAgent({ organizationId: org.id });

      await OrganizationModel.patch(org.id, { defaultAgentId: agent.id });

      const fetched = await OrganizationModel.getById(org.id);
      expect(fetched).not.toBeNull();
      expect(fetched?.defaultAgentId).toBe(agent.id);
    });
  });

  describe("enableSkillToolsForAllOrgs", () => {
    test("flips orgs that haven't opted in and leaves enabled ones untouched", async ({
      makeOrganization,
    }) => {
      const offOrg = await makeOrganization();
      const onOrg = await makeOrganization();
      await OrganizationModel.patch(onOrg.id, { skillToolsEnabled: true });

      const flipped = await OrganizationModel.enableSkillToolsForAllOrgs();

      // Only the org that was off counts as flipped.
      expect(flipped).toBe(1);
      expect(
        (await OrganizationModel.getById(offOrg.id))?.skillToolsEnabled,
      ).toBe(true);
      expect(
        (await OrganizationModel.getById(onOrg.id))?.skillToolsEnabled,
      ).toBe(true);
    });

    test("is idempotent — a second run flips nothing", async ({
      makeOrganization,
    }) => {
      await makeOrganization();

      expect(await OrganizationModel.enableSkillToolsForAllOrgs()).toBe(1);
      expect(await OrganizationModel.enableSkillToolsForAllOrgs()).toBe(0);
    });
  });

  describe("markOnboardingSurveyCompleted", () => {
    test("returns true only for the call that flips the flag", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      expect(
        await OrganizationModel.markOnboardingSurveyCompleted(org.id),
      ).toBe(true);
      // A second submission finds the flag already set and must not re-claim,
      // so the route never forwards the survey to the website twice.
      expect(
        await OrganizationModel.markOnboardingSurveyCompleted(org.id),
      ).toBe(false);

      const [row] = await db
        .select({ at: schema.organizationsTable.onboardingSurveyCompletedAt })
        .from(schema.organizationsTable)
        .where(eq(schema.organizationsTable.id, org.id));
      expect(row?.at).not.toBeNull();
    });

    test("concurrent claims resolve to exactly one winner", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();

      const results = await Promise.all([
        OrganizationModel.markOnboardingSurveyCompleted(org.id),
        OrganizationModel.markOnboardingSurveyCompleted(org.id),
        OrganizationModel.markOnboardingSurveyCompleted(org.id),
      ]);

      // The guarded `WHERE ... IS NULL` update lets only the first committed
      // write claim the flag; the others match zero rows.
      expect(results.filter(Boolean)).toHaveLength(1);
    });
  });
});
