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 { 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 Stream Chat operations
const mockChannelCreate = vi.fn();
const mockChannelUpdate = vi.fn();
const mockChannelDelete = vi.fn();
const mockChannelRemoveMembers = vi.fn();
const mockChannelAddMembers = vi.fn();

// Mock the channel method to return our mocked channel
const mockChannel = {
  create: mockChannelCreate,
  update: mockChannelUpdate,
  delete: mockChannelDelete,
  removeMembers: mockChannelRemoveMembers,
  addMembers: mockChannelAddMembers,
};

// Mock the stream-chat module
vi.mock("stream-chat", () => {
  const mockStreamChatInstance = {
    channel: vi.fn(() => mockChannel),
  };

  return {
    StreamChat: {
      getInstance: vi.fn(() => mockStreamChatInstance),
    },
  };
});

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

describe("Tablo Endpoint", () => {
  // In test mode, createConfig() reads from .env.test
  const config = createConfig();
  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 temporaryUser = getTestUser("temp");

  beforeEach(() => {
    // Reset all mocks before each test
    vi.clearAllMocks();
    mockChannelCreate.mockResolvedValue(undefined);
    mockChannelUpdate.mockResolvedValue(undefined);
    mockChannelDelete.mockResolvedValue(undefined);
    mockChannelRemoveMembers.mockResolvedValue(undefined);
    mockChannelAddMembers.mockResolvedValue(undefined);
    mockSendMail.mockResolvedValue({ messageId: "test-message-id" });
  });

  // Helper function to create tablo
  const createTabloRequest = async (
    user: TestUserData,
    // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
    client: any,
    tabloData: { name: string; status: string; color?: string }
  ) => {
    return await client.tablos.create.$post(
      {
        json: tabloData,
      },
      {
        headers: {
          Authorization: `Bearer ${user.accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );
  };

  // Helper function to update tablo
  const updateTabloRequest = async (
    user: TestUserData,
    // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
    client: any,
    id: string,
    updates: Record<string, unknown>
  ) => {
    return await client.tablos.update.$patch(
      {
        json: { id, ...updates },
      },
      {
        headers: {
          Authorization: `Bearer ${user.accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );
  };

  // Helper function to delete tablo
  // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
  const deleteTabloRequest = async (user: TestUserData, client: any, id: string) => {
    return await client.tablos.delete.$delete(
      {
        json: { id },
      },
      {
        headers: {
          Authorization: `Bearer ${user.accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );
  };

  // Helper function to get tablo members
  // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
  const getTabloMembersRequest = async (user: TestUserData, client: any, tabloId: string) => {
    return await client.tablos.members[":tablo_id"].$get(
      {
        param: { tablo_id: tabloId },
      },
      {
        headers: {
          Authorization: `Bearer ${user.accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );
  };

  // Helper function to leave tablo
  // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
  const leaveTabloRequest = async (user: TestUserData, client: any, tabloId: string) => {
    return await client.tablos.leave.$post(
      {
        json: { tablo_id: tabloId },
      },
      {
        headers: {
          Authorization: `Bearer ${user.accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );
  };

  // Helper function to invite user to tablo
  const inviteToTabloRequest = async (
    user: TestUserData,
    // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
    client: any,
    tabloId: string,
    email: string
  ) => {
    return await client.tablos.invite[":tabloId"].$post(
      {
        param: { tabloId },
        json: { email },
      },
      {
        headers: {
          Authorization: `Bearer ${user.accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );
  };

  describe("POST /tablos/create - Create Tablo", () => {
    const supabaseAdmin = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_ROLE_KEY, {
      auth: { persistSession: false },
    });

    beforeEach(async () => {
      await supabaseAdmin.from("profiles").update({ plan: "standard" }).eq("id", ownerUser.userId);
    });

    it("should allow owner to create a tablo and create a Stream Chat channel", async () => {
      const res = await createTabloRequest(ownerUser, client, {
        name: "New Owner Tablo",
        status: "todo",
        color: "#FF0000",
      });

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.message).toBe("Tablo created successfully");

      // Verify Stream Chat channel was created
      expect(mockChannelCreate).toHaveBeenCalledTimes(1);
      // Verify it was called (the channel is created with tablo data)
      expect(mockChannelCreate).toHaveBeenCalled();
    });

    it("should deny temp user from creating a tablo (regularUserCheck blocks temporary users)", async () => {
      const res = await createTabloRequest(temporaryUser, client, {
        name: "New Temp Tablo",
        status: "in_progress",
        color: "#00FF00",
      });

      // Temporary users are blocked by regularUserCheck middleware
      expect(res.status).toBe(401);
    });

    it("should deny unauthenticated tablo creation", async () => {
      const res = await client.tablos.create.$post({
        json: {
          name: "Unauthorized Tablo",
          status: "todo",
        },
      });

      expect(res.status).toBe(401);
    });

    it("should validate required fields", async () => {
      const res = await client.tablos.create.$post(
        {
          json: {
            // Missing name and status
            color: "#FF0000",
          },
        },
        {
          headers: {
            Authorization: `Bearer ${ownerUser.accessToken}`,
            "Content-Type": "application/json",
          },
        }
      );

      expect(res.status >= 400).toBeTruthy();
    });

    it("should block free plan users who reached the tablo limit", async () => {
      const { data: profileData } = await supabaseAdmin
        .from("profiles")
        .select("plan, organization_id")
        .eq("id", ownerUser.userId)
        .single();

      const originalPlan = profileData?.plan ?? "standard";
      const ownerOrganizationId = profileData?.organization_id;

      if (!ownerOrganizationId) {
        throw new Error("owner organization_id is required for this test");
      }

      await supabaseAdmin.from("profiles").update({ plan: "free" }).eq("id", ownerUser.userId);

      const fillerTablos: Array<{
        owner_id: string;
        organization_id: number;
        name: string;
        status: string;
        color: string;
      }> = [];

      // Ensure the organization has at least 10 tablos before creation attempt.
      const { count: existingCount } = await supabaseAdmin
        .from("tablos")
        .select("id", { count: "exact", head: true })
        .eq("organization_id", ownerOrganizationId)
        .is("deleted_at", null);

      const missingTablos = Math.max(0, 10 - (existingCount || 0));
      for (let i = 0; i < missingTablos; i += 1) {
        fillerTablos.push({
          owner_id: ownerUser.userId,
          organization_id: ownerOrganizationId,
          name: `Limit filler ${i}`,
          status: "todo",
          color: "#DDDDEE",
        });
      }

      if (fillerTablos.length > 0) {
        await supabaseAdmin.from("tablos").insert(fillerTablos);
      }

      try {
        const res = await createTabloRequest(ownerUser, client, {
          name: "Free Limit Tablo",
          status: "todo",
          color: "#ABCDEF",
        });

        expect(res.status).toBe(403);
        const data = await res.json();
        expect(data.error).toBe("You have reached your tablo limit");
        expect(mockChannelCreate).not.toHaveBeenCalled();
      } finally {
        await supabaseAdmin
          .from("profiles")
          .update({ plan: originalPlan })
          .eq("id", ownerUser.userId);
      }
    });
  });

  describe("PATCH /tablos/update - Update Tablo", () => {
    it("should allow owner to update their own tablo", async () => {
      const res = await updateTabloRequest(ownerUser, client, "test_tablo_owner_private", {
        name: "Updated Private Tablo",
      });

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.message).toBe("Tablo updated successfully");
    });

    it("should deny temp user from updating their own tablo (regularUserCheck blocks temporary users)", async () => {
      const res = await updateTabloRequest(temporaryUser, client, "test_tablo_temp_private", {
        name: "Updated Temp Tablo",
        status: "done",
      });

      // Temporary users are blocked by regularUserCheck middleware
      expect(res.status).toBe(401);
    });

    it("should deny owner from updating temp user's tablo", async () => {
      const res = await updateTabloRequest(ownerUser, client, "test_tablo_temp_private", {
        name: "Should Not Update",
      });

      expect(res.status).toBe(403);
    });

    it("should deny temp user from updating owner's tablo (regularUserCheck blocks temporary users)", async () => {
      const res = await updateTabloRequest(temporaryUser, client, "test_tablo_owner_private", {
        name: "Should Not Update",
      });

      // Temporary users are blocked by regularUserCheck middleware
      expect(res.status).toBe(401);
    });

    it("should deny unauthenticated tablo update", async () => {
      const res = await client.tablos.update.$patch({
        json: {
          id: "test_tablo_owner_private",
          name: "Unauthorized Update",
        },
      });

      expect(res.status).toBe(401);
    });

    it("should handle non-existent tablo update", async () => {
      const res = await updateTabloRequest(ownerUser, client, "non_existent_tablo", {
        name: "Should Not Update",
      });

      expect(res.status).toBe(403);
    });
  });

  describe("DELETE /tablos/delete - Delete Tablo", () => {
    it("should allow owner with admin access to delete tablo and delete Stream Chat channel", async () => {
      // Owner has admin access to their tablos
      const res = await deleteTabloRequest(ownerUser, client, "test_tablo_owner_private");

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.message).toBe("Tablo deleted successfully");

      // Verify Stream Chat channel was deleted
      expect(mockChannelDelete).toHaveBeenCalledTimes(1);
      expect(mockChannelDelete).toHaveBeenCalled();
    });

    it("should deny temp user without admin access from deleting tablo", async () => {
      // Temp has non-admin access to owner's shared tablo
      const res = await deleteTabloRequest(temporaryUser, client, "test_tablo_owner_shared");

      expect(res.status).toBe(403);
      const data = await res.json();
      expect(data.error).toBe("You are not authorized to delete this tablo");
    });

    it("should deny deletion of tablo user doesn't have access to", async () => {
      const res = await deleteTabloRequest(temporaryUser, client, "test_tablo_owner_private");

      expect(res.status).toBe(403);
      const data = await res.json();
      expect(data.error).toBe("You are not authorized to delete this tablo");
    });

    it("should deny unauthenticated tablo deletion", async () => {
      const res = await client.tablos.delete.$delete({
        json: {
          id: "test_tablo_owner_private",
        },
      });

      expect(res.status).toBe(401);
    });

    it("should handle non-existent tablo deletion", async () => {
      const res = await deleteTabloRequest(ownerUser, client, "non_existent_tablo");

      expect(res.status).toBe(403);
    });
  });

  describe("GET /tablos/members/:tablo_id - Get Tablo Members", () => {
    it("should allow owner to get members of their own tablo", async () => {
      const res = await getTabloMembersRequest(ownerUser, client, "test_tablo_owner_shared");

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.members).toBeDefined();
      expect(Array.isArray(data.members)).toBeTruthy();
    });

    it("should allow temp user to get members of their own tablo", async () => {
      const res = await getTabloMembersRequest(temporaryUser, client, "test_tablo_temp_private");

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.members).toBeDefined();
      expect(Array.isArray(data.members)).toBeTruthy();
    });

    it("should allow user to get members of shared tablo they have access to", async () => {
      const res = await getTabloMembersRequest(temporaryUser, client, "test_tablo_owner_shared");

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.members).toBeDefined();
      expect(Array.isArray(data.members)).toBeTruthy();
    });

    it("should allow owner to get members of tablo with admin access", async () => {
      const res = await getTabloMembersRequest(ownerUser, client, "test_tablo_temp_shared_admin");

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.members).toBeDefined();
      expect(Array.isArray(data.members)).toBeTruthy();
    });

    it("should deny access to members of tablo without permission", async () => {
      const res = await getTabloMembersRequest(temporaryUser, client, "test_tablo_owner_private");

      expect(res.status).toBe(403);
    });

    it("should deny unauthenticated access to tablo members", async () => {
      const res = await client.tablos.members[":tablo_id"].$get({
        param: { tablo_id: "test_tablo_owner_private" },
      });

      expect(res.status).toBe(401);
    });

    it("should handle non-existent tablo", async () => {
      const res = await getTabloMembersRequest(ownerUser, client, "non_existent_tablo");

      expect(res.status).toBe(403);
    });
  });

  describe("Access Control - Cross-user Access", () => {
    it("owner should not access temp's private tablo members", async () => {
      const res = await getTabloMembersRequest(ownerUser, client, "test_tablo_temp_private");

      expect(res.status).toBe(403);
    });

    it("temp should not access owner's private tablo members", async () => {
      const res = await getTabloMembersRequest(temporaryUser, client, "test_tablo_owner_private_2");

      expect(res.status).toBe(403);
    });

    it("temp should access owner's team tablo members", async () => {
      const res = await getTabloMembersRequest(temporaryUser, client, "test_tablo_owner_team");

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

    it("owner should access temp's admin shared tablo members", async () => {
      const res = await getTabloMembersRequest(ownerUser, client, "test_tablo_temp_shared_admin");

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

  describe("POST /tablos/leave - Leave Tablo", () => {
    it("should allow temp user to leave a shared tablo and remove from Stream Chat channel", async () => {
      const res = await leaveTabloRequest(temporaryUser, client, "test_tablo_owner_team");

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.message).toBe("Tablo left successfully");

      // Verify Stream Chat channel removeMembers was called
      expect(mockChannelRemoveMembers).toHaveBeenCalledTimes(1);
      expect(mockChannelRemoveMembers).toHaveBeenCalledWith([temporaryUser.userId]);
    });

    it("should allow owner to leave a tablo and remove from Stream Chat channel", async () => {
      const res = await leaveTabloRequest(ownerUser, client, "test_tablo_temp_shared_admin");

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.message).toBe("Tablo left successfully");

      // Verify Stream Chat channel removeMembers was called
      expect(mockChannelRemoveMembers).toHaveBeenCalledTimes(1);
      expect(mockChannelRemoveMembers).toHaveBeenCalledWith([ownerUser.userId]);
    });

    it("should deny unauthenticated leave request", async () => {
      const res = await client.tablos.leave.$post({
        json: {
          tablo_id: "test_tablo_owner_shared",
        },
      });

      expect(res.status).toBe(401);
    });
  });

  describe("POST /tablos/invite/:tabloId - Invite User to Tablo", () => {
    it("should create a notification when inviting an existing user to a tablo", async () => {
      // Create a Supabase client to query the database
      const supabaseAdmin = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_ROLE_KEY, {
        auth: { persistSession: false },
      });

      // Get temp user's email
      const tempUserEmail = temporaryUser.email;

      // Count notifications for temp user before invite
      const { data: notificationsBefore } = await supabaseAdmin
        .from("notifications")
        .select("*")
        .eq("user_id", temporaryUser.userId)
        .eq("entity_type", "tablo_invites");

      const countBefore = notificationsBefore?.length || 0;

      // Owner invites temp user to owner's private tablo (temp doesn't have access yet)
      const res = await inviteToTabloRequest(
        ownerUser,
        client,
        "test_tablo_owner_private",
        tempUserEmail
      );

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.message).toContain("sent successfully");

      // Query notifications table to verify notification was created
      const { data: notificationsAfter } = await supabaseAdmin
        .from("notifications")
        .select("*")
        .eq("user_id", temporaryUser.userId)
        .eq("entity_type", "tablo_invites")
        .order("created_at", { ascending: false });

      expect(notificationsAfter).toBeDefined();
      expect(notificationsAfter?.length).toBeGreaterThan(countBefore);

      // Verify the latest notification has correct properties
      const latestNotification = notificationsAfter?.[0];
      expect(latestNotification).toBeDefined();
      expect(latestNotification?.entity_type).toBe("tablo_invites");
      expect(latestNotification?.user_id).toBe(temporaryUser.userId);
      expect(latestNotification?.actor_id).toBe(ownerUser.userId);
      expect(latestNotification?.action_type).toBe("created");
      // Message is now a JSONB object with en/fr keys
      expect(latestNotification?.message).toBeDefined();
      // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
      expect((latestNotification?.message as any)?.en).toContain("invited");
      // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
      expect((latestNotification?.message as any)?.fr).toContain("invité");
      expect(latestNotification?.read_at).toBeNull();
    });

    it("should create notification when inviting non-existent user (creates temporary account)", async () => {
      // Create a Supabase client to query the database
      const supabaseAdmin = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_ROLE_KEY, {
        auth: { persistSession: false },
      });

      const nonExistentEmail = "nonexistent_user_test@example.com";

      // Owner invites non-existent user to owner's private tablo
      const res = await inviteToTabloRequest(
        ownerUser,
        client,
        "test_tablo_owner_private_2",
        nonExistentEmail
      );

      // Request should succeed (creates invite)
      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.message).toContain("sent successfully");

      // Verify user was created with a temporary account
      const { data: authUsers } = await supabaseAdmin.auth.admin.listUsers();
      const createdUser = authUsers.users.find(
        (u: { email: string }) => u.email === nonExistentEmail
      );
      expect(createdUser).toBeDefined();

      // Check if notification was created for the newly created user
      // Since the system creates a temporary account, a notification should be created
      const { data: notificationsForInvite } = await supabaseAdmin
        .from("notifications")
        .select("*")
        .eq("user_id", createdUser?.id)
        .eq("entity_type", "tablo_invites")
        .contains("metadata", { invited_email: nonExistentEmail });

      // Should create notification for the newly created temporary user
      expect(notificationsForInvite?.length || 0).toBeGreaterThan(0);
      // Message is now a JSONB object with en/fr keys
      // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
      expect((notificationsForInvite?.[0].message as any)?.en).toContain("invited");
      // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
      expect((notificationsForInvite?.[0].message as any)?.fr).toContain("invité");
    });
  });
});
