import { createClient } from "@supabase/supabase-js";
import { testClient } from "hono/testing";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createConfig } from "../../config.js";
import { ensureActiveClientAccess, upsertClientByEmail } from "../../helpers/clientAccounts.js";
import { createClientMagicLink } from "../../helpers/clientMagicLinks.js";
import { MiddlewareManager } from "../../middlewares/middleware.js";
import { getMainRouter } from "../../routers/index.js";
import type { TestUserData } from "../helpers/dbSetup.js";
import { getTestUser } from "../helpers/dbSetup.js";

// Mock nodemailer
const mockSendMail = vi.fn();
vi.mock("nodemailer", () => ({
  default: {
    createTransport: vi.fn(() => ({
      sendMail: mockSendMail,
    })),
  },
  createTransport: vi.fn(() => ({
    sendMail: mockSendMail,
  })),
}));

const config = createConfig();
const supabaseAdmin = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_ROLE_KEY, {
  auth: { persistSession: false },
});
const { error: clientSchemaError } = await supabaseAdmin.from("clients").select("id").limit(1);
const hasClientAuthSchema =
  !clientSchemaError || (clientSchemaError as { code?: string }).code !== "PGRST205";

describe.skipIf(!hasClientAuthSchema)("Client Invites Endpoints", () => {
  MiddlewareManager.initialize(config);
  const app = getMainRouter(config);
  // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
  const client = testClient(app) as any;

  const ownerUser = getTestUser("owner");
  const tempUser = getTestUser("temp");

  // The owner has admin access to this tablo (created via TEST_TABLOS with owner_key: "owner")
  const adminTabloId = "test_tablo_owner_private";

  beforeEach(() => {
    vi.clearAllMocks();
    mockSendMail.mockResolvedValue({ messageId: "test-message-id" });
  });

  // ─── Helpers ────────────────────────────────────────────────────────────────

  const postInvite = (user: TestUserData, tabloId: string, email: string) =>
    client["client-invites"][":tabloId"].$post(
      { param: { tabloId }, json: { email } },
      { headers: { Authorization: `Bearer ${user.accessToken}` } }
    );

  const getPending = (user: TestUserData, tabloId: string) =>
    client["client-invites"][":tabloId"].pending.$get(
      { param: { tabloId } },
      { headers: { Authorization: `Bearer ${user.accessToken}` } }
    );

  const deleteInvite = (user: TestUserData, tabloId: string, inviteId: number) =>
    client["client-invites"][":tabloId"][":inviteId"].$delete(
      { param: { tabloId, inviteId: String(inviteId) } },
      { headers: { Authorization: `Bearer ${user.accessToken}` } }
    );

  const getSetupInvite = (token: string) =>
    client["client-invites"].setup[":token"].$get({
      param: { token },
    });

  const completeSetupInvite = (token: string, password: string) =>
    client["client-invites"].setup[":token"].$post({
      param: { token },
      json: { password },
    });

  const insertClientInvite = async (opts: {
    tabloId: string;
    invitedEmail: string;
    invitedBy: string;
    token: string;
    inviteType?: string;
    isPending?: boolean;
    expiresAt?: string;
  }) => {
    const expiresAt = opts.expiresAt ?? new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString();

    const { data, error } = await supabaseAdmin
      .from("client_invites")
      .insert({
        tablo_id: opts.tabloId,
        invited_email: opts.invitedEmail,
        invited_by: opts.invitedBy,
        invite_token: opts.token,
        invite_type: opts.inviteType ?? "setup",
        is_pending: opts.isPending ?? true,
        expires_at: expiresAt,
      })
      .select("id")
      .single();

    if (error) throw new Error(`Failed to insert client_invite: ${error.message}`);
    return data.id as number;
  };

  const insertClientMagicLinkInvite = async (opts: {
    tabloId: string;
    invitedEmail: string;
    invitedBy: string;
    expiresInMinutes?: number;
  }) => {
    const clientResult = await upsertClientByEmail(supabaseAdmin, opts.invitedEmail);
    if (!clientResult.client) {
      throw new Error(clientResult.error ?? "Failed to upsert client");
    }

    const accessResult = await ensureActiveClientAccess(supabaseAdmin, {
      clientId: clientResult.client.id,
      grantedBy: opts.invitedBy,
      tabloId: opts.tabloId,
    });

    if (!accessResult.success) {
      throw new Error(accessResult.error ?? "Failed to grant client access");
    }

    const magicLinkResult = await createClientMagicLink(supabaseAdmin, {
      clientId: clientResult.client.id,
      createdBy: opts.invitedBy,
      email: clientResult.client.email,
      expiresInMinutes: opts.expiresInMinutes ?? 30,
      jwtSecret: config.CLIENT_AUTH_JWT_SECRET,
      purpose: "invite",
      redirectTo: `/tablo/${opts.tabloId}`,
      tabloId: opts.tabloId,
    });

    if (!magicLinkResult.link) {
      throw new Error(magicLinkResult.error ?? "Failed to create client magic link");
    }

    return {
      clientId: clientResult.client.id,
      inviteId: magicLinkResult.link.id as number,
      token: magicLinkResult.token as string,
    };
  };

  const cleanupInvitesByEmail = async (email: string) => {
    await supabaseAdmin.from("client_invites").delete().eq("invited_email", email);

    const { data: usersData } = await supabaseAdmin.auth.admin.listUsers();
    // biome-ignore lint/suspicious/noExplicitAny: admin.listUsers returns typed data at runtime
    const users = usersData as any;
    // biome-ignore lint/suspicious/noExplicitAny: admin user type
    const clientUser = users?.users?.find((u: any) => u.email === email);
    if (clientUser) {
      await supabaseAdmin.from("tablo_access").delete().eq("user_id", clientUser.id);
      await supabaseAdmin.auth.admin.deleteUser(clientUser.id);
    }
  };

  const cleanupClientAuthByEmail = async (email: string) => {
    const normalizedEmail = email.trim().toLowerCase();

    const { data: client } = await supabaseAdmin
      .from("clients")
      .select("id")
      .eq("normalized_email", normalizedEmail)
      .maybeSingle();

    if (!client?.id) {
      return;
    }

    await supabaseAdmin.from("client_magic_links").delete().eq("client_id", client.id);
    await supabaseAdmin.from("client_access").delete().eq("client_id", client.id);
    await supabaseAdmin.from("clients").delete().eq("id", client.id);
  };

  const createClientAccount = async (
    email: string,
    input?: { onboarded?: boolean; password?: string }
  ) => {
    const password = input?.password ?? "client_password_123";
    const { data: authData, error: authError } = await supabaseAdmin.auth.admin.createUser({
      email,
      password,
      email_confirm: true,
      user_metadata: { role: "client" },
    });

    if (authError || !authData?.user) {
      throw new Error(`Failed to create client account: ${authError?.message}`);
    }

    const updates: Record<string, unknown> = { is_client: true };
    if (input?.onboarded) {
      updates.client_onboarded_at = new Date().toISOString();
    }

    const { error: profileError } = await supabaseAdmin
      .from("profiles")
      .update(updates)
      .eq("id", authData.user.id);

    if (profileError) {
      throw new Error(`Failed to update client profile: ${profileError.message}`);
    }

    return authData.user;
  };

  // ════════════════════════════════════════════════════════════════════════════
  // POST /:tabloId — Create client invite
  // ════════════════════════════════════════════════════════════════════════════

  describe("POST /client-invites/:tabloId", () => {
    const testEmail = "test_client_invite_new@example.com";
    const existingClientEmail = "test_existing_client_invite@example.com";

    beforeEach(async () => {
      await cleanupInvitesByEmail(testEmail);
      await cleanupInvitesByEmail(existingClientEmail);
      await cleanupClientAuthByEmail(testEmail);
      await cleanupClientAuthByEmail(existingClientEmail);
    });

    it("upserts one global client identity per normalized email", async () => {
      const mixedCaseEmail = "Test_Client_Invite_New@Example.com";

      const res = await postInvite(ownerUser, adminTabloId, mixedCaseEmail);

      expect(res.status).toBe(200);

      const { data: clients, error } = await supabaseAdmin
        .from("clients")
        .select("id, email, normalized_email")
        .eq("normalized_email", mixedCaseEmail.toLowerCase());

      expect(error).toBeNull();
      expect(clients).toHaveLength(1);
      expect(clients?.[0]?.email).toBe(mixedCaseEmail.toLowerCase());
      expect(clients?.[0]?.normalized_email).toBe(mixedCaseEmail.toLowerCase());
    });

    it("creates a client_access grant for the invited tablo", async () => {
      const res = await postInvite(ownerUser, adminTabloId, testEmail);

      expect(res.status).toBe(200);

      const { data: client } = await supabaseAdmin
        .from("clients")
        .select("id")
        .eq("normalized_email", testEmail)
        .single();

      const { data: accessRows, error } = await supabaseAdmin
        .from("client_access")
        .select("id, tablo_id, client_id, revoked_at")
        .eq("client_id", client.id)
        .eq("tablo_id", adminTabloId);

      expect(error).toBeNull();
      expect(accessRows).toHaveLength(1);
      expect(accessRows?.[0]?.revoked_at).toBeNull();
    });

    it("creates a one-time invite magic link row", async () => {
      const res = await postInvite(ownerUser, adminTabloId, testEmail);

      expect(res.status).toBe(200);

      const { data: client } = await supabaseAdmin
        .from("clients")
        .select("id")
        .eq("normalized_email", testEmail)
        .single();

      const { data: links, error } = await supabaseAdmin
        .from("client_magic_links")
        .select("id, client_id, purpose, consumed_at, expires_at, created_by")
        .eq("client_id", client.id)
        .eq("purpose", "invite");

      expect(error).toBeNull();
      expect(links).toHaveLength(1);
      expect(links?.[0]?.consumed_at).toBeNull();
      expect(links?.[0]?.expires_at).toBeTruthy();
      expect(links?.[0]?.created_by).toBe(ownerUser.userId);
    });

    it("reuses the same client row when the same email is invited again", async () => {
      const firstTabloId = adminTabloId;
      const secondTabloId = "test_tablo_owner_shared";

      const firstRes = await postInvite(ownerUser, firstTabloId, existingClientEmail);
      expect(firstRes.status).toBe(200);

      const { data: firstClient } = await supabaseAdmin
        .from("clients")
        .select("id")
        .eq("normalized_email", existingClientEmail)
        .single();

      const secondRes = await postInvite(ownerUser, secondTabloId, existingClientEmail);
      expect(secondRes.status).toBe(200);

      const { data: secondClient } = await supabaseAdmin
        .from("clients")
        .select("id")
        .eq("normalized_email", existingClientEmail)
        .single();

      const { data: accessRows, error } = await supabaseAdmin
        .from("client_access")
        .select("tablo_id")
        .eq("client_id", firstClient.id);

      expect(error).toBeNull();
      expect(secondClient.id).toBe(firstClient.id);
      expect(accessRows?.map((row) => row.tablo_id).sort()).toEqual(
        [firstTabloId, secondTabloId].sort()
      );
    });

    it("emails a client-app exchange URL instead of the API host", async () => {
      const res = await postInvite(ownerUser, adminTabloId, testEmail);

      expect(res.status).toBe(200);

      const sentMail = mockSendMail.mock.calls[0]?.[0];
      expect(sentMail?.to).toBe(testEmail);
      expect(sentMail?.html).toContain(`${config.CLIENTS_URL}/auth/exchange?token=`);
      expect(sentMail?.html).not.toContain(`${config.API_BASE_URL}/client-auth/exchange?token=`);
    });

    it("rejects emails already used by a main-app account", async () => {
      const res = await postInvite(ownerUser, adminTabloId, ownerUser.email);

      expect(res.status).toBe(409);
      const data = await res.json();
      expect(data.error).toContain("already belongs");
    });

    it("rejects temporary users before admin check", async () => {
      const res = await postInvite(tempUser, adminTabloId, testEmail);
      expect(res.status).toBe(403);
    });

    it("returns 400 for an invalid email", async () => {
      const res = await postInvite(ownerUser, adminTabloId, "not-an-email");
      expect(res.status).toBe(400);
      const data = await res.json();
      expect(data.error).toContain("valid email");
    });

    it("returns 400 for a missing email", async () => {
      const res = client["client-invites"][":tabloId"].$post(
        { param: { tabloId: adminTabloId }, json: {} },
        { headers: { Authorization: `Bearer ${ownerUser.accessToken}` } }
      );
      expect((await res).status).toBe(400);
    });

    it("returns 401 for unauthenticated requests", async () => {
      const res = await client["client-invites"][":tabloId"].$post({
        param: { tabloId: adminTabloId },
        json: { email: testEmail },
      });
      expect(res.status).toBe(401);
    });
  });

  // ════════════════════════════════════════════════════════════════════════════
  // GET/POST /setup/:token — Validate and complete a client setup invite
  // ════════════════════════════════════════════════════════════════════════════

  describe("GET/POST /client-invites/setup/:token", () => {
    const setupEmail = "test_client_setup@example.com";

    beforeEach(async () => {
      await cleanupInvitesByEmail(setupEmail);
    });

    it("returns invite metadata for a valid pending setup token", async () => {
      const token = `test_setup_valid_${Date.now()}`;
      await insertClientInvite({
        tabloId: adminTabloId,
        invitedEmail: setupEmail,
        invitedBy: ownerUser.userId,
        token,
      });

      try {
        const res = await getSetupInvite(token);
        expect(res.status).toBe(200);
        const data = await res.json();
        expect(data.email).toBe(setupEmail);
        expect(data.tabloId).toBe(adminTabloId);
      } finally {
        await supabaseAdmin.from("client_invites").delete().eq("invite_token", token);
      }
    });

    it("returns 410 for an expired invite", async () => {
      const token = `test_expired_${Date.now()}`;
      const pastDate = new Date(Date.now() - 1000).toISOString();

      await insertClientInvite({
        tabloId: adminTabloId,
        invitedEmail: setupEmail,
        invitedBy: ownerUser.userId,
        token,
        expiresAt: pastDate,
      });

      try {
        const res = await getSetupInvite(token);
        expect(res.status).toBe(410);
        const data = await res.json();
        expect(data.error).toContain("expired");
      } finally {
        await supabaseAdmin.from("client_invites").delete().eq("invite_token", token);
      }
    });

    it("completes password setup once and rejects reuse", async () => {
      const token = `test_setup_complete_${Date.now()}`;
      await createClientAccount(setupEmail);
      await insertClientInvite({
        tabloId: adminTabloId,
        invitedEmail: setupEmail,
        invitedBy: ownerUser.userId,
        token,
      });

      try {
        const res = await completeSetupInvite(token, "new_password_123");
        expect(res.status).toBe(200);
        const data = await res.json();
        expect(data.success).toBe(true);
        expect(data.email).toBe(setupEmail);
        expect(data.tabloId).toBe(adminTabloId);

        const reused = await completeSetupInvite(token, "new_password_456");
        expect(reused.status).toBe(404);
      } finally {
        await supabaseAdmin.from("client_invites").delete().eq("invite_token", token);
      }
    });

    it("returns 404 for a non-existent token", async () => {
      const res = await getSetupInvite("nonexistent_token_xyz");
      expect(res.status).toBe(404);
    });

    it("marks cancelled pending setup tokens unusable", async () => {
      const token = `test_setup_cancelled_${Date.now()}`;
      const inviteId = await insertClientInvite({
        tabloId: adminTabloId,
        invitedEmail: setupEmail,
        invitedBy: ownerUser.userId,
        token,
      });

      const cancelRes = await deleteInvite(ownerUser, adminTabloId, inviteId);
      expect(cancelRes.status).toBe(200);

      const res = await getSetupInvite(token);
      expect(res.status).toBe(404);
    });
  });

  // ════════════════════════════════════════════════════════════════════════════
  // GET /:tabloId/pending — List pending client invites
  // ════════════════════════════════════════════════════════════════════════════

  describe("GET /client-invites/:tabloId/pending", () => {
    const pendingEmail = "test_client_pending_list@example.com";
    let insertedId: number;

    beforeEach(async () => {
      await cleanupInvitesByEmail(pendingEmail);
      await cleanupClientAuthByEmail(pendingEmail);
      const invite = await insertClientMagicLinkInvite({
        tabloId: adminTabloId,
        invitedEmail: pendingEmail,
        invitedBy: ownerUser.userId,
      });
      insertedId = invite.inviteId;
    });

    it("returns pending invites for an admin", async () => {
      const res = await getPending(ownerUser, adminTabloId);
      expect(res.status).toBe(200);
      const data = await res.json();
      expect(Array.isArray(data.invites)).toBe(true);

      const found = data.invites.find((inv: { id: number }) => inv.id === insertedId);
      expect(found).toBeDefined();
      expect(found.invited_email).toBe(pendingEmail);
      expect(found.is_pending).toBe(true);
    });

    it("returns 403 for a temporary user before admin check", async () => {
      const res = await getPending(tempUser, adminTabloId);
      expect(res.status).toBe(403);
    });

    it("returns 401 for unauthenticated requests", async () => {
      const res = await client["client-invites"][":tabloId"].pending.$get({
        param: { tabloId: adminTabloId },
      });
      expect(res.status).toBe(401);
    });
  });

  // ════════════════════════════════════════════════════════════════════════════
  // DELETE /:tabloId/:inviteId — Cancel a client invite
  // ════════════════════════════════════════════════════════════════════════════

  describe("DELETE /client-invites/:tabloId/:inviteId", () => {
    const cancelEmail = "test_client_cancel@example.com";

    beforeEach(async () => {
      await cleanupInvitesByEmail(cancelEmail);
      await cleanupClientAuthByEmail(cancelEmail);
    });

    it("cancels a pending invite and revokes client access", async () => {
      const invite = await insertClientMagicLinkInvite({
        tabloId: adminTabloId,
        invitedEmail: cancelEmail,
        invitedBy: ownerUser.userId,
      });

      const res = await deleteInvite(ownerUser, adminTabloId, invite.inviteId);
      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.success).toBe(true);

      const { data: cancelledLink } = await supabaseAdmin
        .from("client_magic_links")
        .select("consumed_at")
        .eq("id", invite.inviteId)
        .single();

      const { data: accessRow } = await supabaseAdmin
        .from("client_access")
        .select("revoked_at")
        .eq("client_id", invite.clientId)
        .eq("tablo_id", adminTabloId)
        .single();

      expect(cancelledLink?.consumed_at).toBeTruthy();
      expect(accessRow?.revoked_at).toBeTruthy();
    });

    it("returns 403 for a temporary user before admin check", async () => {
      const invite = await insertClientMagicLinkInvite({
        tabloId: adminTabloId,
        invitedEmail: cancelEmail,
        invitedBy: ownerUser.userId,
      });

      const res = await deleteInvite(tempUser, adminTabloId, invite.inviteId);
      expect(res.status).toBe(403);
    });

    it("returns 404 for a non-existent invite", async () => {
      const res = await deleteInvite(ownerUser, adminTabloId, 999999);
      expect(res.status).toBe(404);
    });

    it("returns 400 for an already-cancelled invite", async () => {
      const invite = await insertClientMagicLinkInvite({
        tabloId: adminTabloId,
        invitedEmail: cancelEmail,
        invitedBy: ownerUser.userId,
      });

      await supabaseAdmin
        .from("client_magic_links")
        .update({ consumed_at: new Date().toISOString() })
        .eq("id", invite.inviteId);

      const res = await deleteInvite(ownerUser, adminTabloId, invite.inviteId);
      expect(res.status).toBe(400);
      const data = await res.json();
      expect(data.error).toContain("pending");
    });

    it("returns 401 for unauthenticated requests", async () => {
      const res = await client["client-invites"][":tabloId"][":inviteId"].$delete({
        param: { tabloId: adminTabloId, inviteId: "1" },
      });
      expect(res.status).toBe(401);
    });
  });
});
