import { describe, expect, it, vi, test, beforeEach, afterEach } from "vitest";
import { render, screen, within, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import userEvent from "@testing-library/user-event";
import { createRoutesStub } from "react-router";
import { selectOrganization } from "test-utils";
import { organizationService } from "#/api/organization-service/organization-service.api";
import ManageOrganizationMembers from "#/routes/manage-organization-members";
import SettingsScreen, {
  clientLoader as settingsClientLoader,
} from "#/routes/settings";
import {
  ORGS_AND_MEMBERS,
  resetOrgMockData,
  resetOrgsAndMembersMockData,
} from "#/mocks/org-handlers";
import OptionService from "#/api/option-service/option-service.api";

function ManageOrganizationMembersWithPortalRoot() {
  return (
    <div>
      <ManageOrganizationMembers />
      <div data-testid="portal-root" id="portal-root" />
    </div>
  );
}

const RouteStub = createRoutesStub([
  {
    // @ts-expect-error - ignoreing error for test stub
    loader: settingsClientLoader,
    Component: SettingsScreen,
    path: "/settings",
    HydrateFallback: () => <div>Loading...</div>,
    children: [
      {
        Component: ManageOrganizationMembersWithPortalRoot,
        path: "/settings/org-members",
      },
      {
        Component: () => <div data-testid="user-settings" />,
        path: "/settings/member",
      },
    ],
  },
]);

let queryClient: QueryClient;

describe("Manage Organization Members Route", () => {
  const getMeSpy = vi.spyOn(organizationService, "getMe");

  beforeEach(() => {
    const getConfigSpy = vi.spyOn(OptionService, "getConfig");
    // @ts-expect-error - only return APP_MODE for these tests
    getConfigSpy.mockResolvedValue({
      APP_MODE: "saas",
    });

    queryClient = new QueryClient();

    // Set default mock for user (admin role has invite permission)
    getMeSpy.mockResolvedValue({
      org_id: "1",
      user_id: "1",
      email: "test@example.com",
      role: "admin",
      llm_api_key: "**********",
      max_iterations: 20,
      llm_model: "gpt-4",
      llm_api_key_for_byor: null,
      llm_base_url: "https://api.openai.com",
      status: "active",
    });
  });

  afterEach(() => {
    vi.restoreAllMocks();
    // Reset organization mock data to ensure clean state between tests
    resetOrgMockData();
    // Reset ORGS_AND_MEMBERS to initial state
    resetOrgsAndMembersMockData();
    // Clear queryClient cache to ensure fresh data for next test
    queryClient.clear();
  });

  const renderManageOrganizationMembers = () =>
    render(<RouteStub initialEntries={["/settings/org-members"]} />, {
      wrapper: ({ children }) => (
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
      ),
    });

  // Helper function to find a member by email
  const findMemberByEmail = async (email: string) => {
    const memberListItems = await screen.findAllByTestId("member-item");
    const member = memberListItems.find((item) =>
      within(item).queryByText(email),
    );
    if (!member) {
      throw new Error(`Could not find member with email: ${email}`);
    }
    return member;
  };

  // Helper function to open role dropdown for a member
  const openRoleDropdown = async (
    memberElement: HTMLElement,
    roleText: string,
  ) => {
    // Find the role text that's clickable (has cursor-pointer class or is the main role display)
    // Use a more specific query to avoid matching dropdown options
    const roleElement = within(memberElement).getByText(
      new RegExp(`^${roleText}$`, "i"),
    );
    await userEvent.click(roleElement);
    return within(memberElement).getByTestId(
      "organization-member-role-context-menu",
    );
  };

  // Helper function to change member role
  const changeMemberRole = async (
    memberElement: HTMLElement,
    currentRole: string,
    newRole: string,
  ) => {
    const dropdown = await openRoleDropdown(memberElement, currentRole);
    const roleOption = within(dropdown).getByText(new RegExp(newRole, "i"));
    await userEvent.click(roleOption);
  };

  // Helper function to verify dropdown is not visible
  const expectDropdownNotVisible = (memberElement: HTMLElement) => {
    expect(
      within(memberElement).queryByTestId(
        "organization-member-role-context-menu",
      ),
    ).not.toBeInTheDocument();
  };

  // Helper function to setup test with user and organization
  const setupTestWithUserAndOrg = async (
    userData: {
      org_id: string;
      user_id: string;
      email: string;
      role: "owner" | "admin" | "member";
      llm_api_key: string;
      max_iterations: number;
      llm_model: string;
      llm_api_key_for_byor: string | null;
      llm_base_url: string;
      status: "active" | "invited" | "inactive";
    },
    orgIndex: number,
  ) => {
    getMeSpy.mockResolvedValue(userData);
    renderManageOrganizationMembers();
    await screen.findByTestId("manage-organization-members-settings");
    await selectOrganization({ orgIndex });
  };

  // Helper function to create updateMember spy
  const createUpdateMemberRoleSpy = () =>
    vi.spyOn(organizationService, "updateMember");

  // Helper function to verify role change is not permitted
  const verifyRoleChangeNotPermitted = async (
    userData: {
      org_id: string;
      user_id: string;
      email: string;
      role: "owner" | "admin" | "member";
      llm_api_key: string;
      max_iterations: number;
      llm_model: string;
      llm_api_key_for_byor: string | null;
      llm_base_url: string;
      status: "active" | "invited" | "inactive";
    },
    orgIndex: number,
    targetMemberIndex: number,
    expectedRoleText: string,
  ) => {
    await setupTestWithUserAndOrg(userData, orgIndex);

    const memberListItems = await screen.findAllByTestId("member-item");
    const targetMember = memberListItems[targetMemberIndex];
    const roleText = within(targetMember).getByText(
      new RegExp(`^${expectedRoleText}$`, "i"),
    );
    expect(roleText).toBeInTheDocument();
    await userEvent.click(roleText);

    // Verify that the dropdown does not open
    expectDropdownNotVisible(targetMember);
  };

  // Helper function to setup invite test (render and select organization)
  const setupInviteTest = async (orgIndex: number = 0) => {
    renderManageOrganizationMembers();
    await selectOrganization({ orgIndex });
  };

  // Helper function to setup test with organization (waits for settings screen)
  const setupTestWithOrg = async (orgIndex: number = 0) => {
    renderManageOrganizationMembers();
    await screen.findByTestId("manage-organization-members-settings");
    await selectOrganization({ orgIndex });
  };

  // Helper function to find invite button
  const findInviteButton = async () =>
    await screen.findByRole("button", {
      name: /ORG\$INVITE_ORGANIZATION_MEMBER/i,
    });

  // Helper function to verify all three role options are present in dropdown
  const expectAllRoleOptionsPresent = (dropdown: HTMLElement) => {
    expect(within(dropdown).getByText(/owner/i)).toBeInTheDocument();
    expect(within(dropdown).getByText(/admin/i)).toBeInTheDocument();
    expect(within(dropdown).getByText(/member/i)).toBeInTheDocument();
  };

  // Helper function to close dropdown by clicking outside
  const closeDropdown = async () => {
    await userEvent.click(document.body);
  };

  // Helper function to verify owner option is not present in dropdown
  const expectOwnerOptionNotPresent = (dropdown: HTMLElement) => {
    expect(within(dropdown).queryByText(/owner/i)).not.toBeInTheDocument();
  };

  it("should render", async () => {
    renderManageOrganizationMembers();
    await screen.findByTestId("manage-organization-members-settings");
  });

  it("should navigate away from the page if not saas", async () => {
    const getConfigSpy = vi.spyOn(OptionService, "getConfig");
    // @ts-expect-error - only return APP_MODE for these tests
    getConfigSpy.mockResolvedValue({
      APP_MODE: "oss",
    });

    renderManageOrganizationMembers();
    expect(
      screen.queryByTestId("manage-organization-members-settings"),
    ).not.toBeInTheDocument();
  });

  it("should allow the user to select an organization", async () => {
    const getOrganizationMembersSpy = vi.spyOn(
      organizationService,
      "getOrganizationMembers",
    );

    renderManageOrganizationMembers();
    await screen.findByTestId("manage-organization-members-settings");

    expect(getOrganizationMembersSpy).not.toHaveBeenCalled();

    await selectOrganization({ orgIndex: 0 });
    expect(getOrganizationMembersSpy).toHaveBeenCalledExactlyOnceWith({
      orgId: "1",
    });
  });

  it("should render the list of organization members", async () => {
    await setupTestWithOrg(0);
    const members = ORGS_AND_MEMBERS["1"];

    const memberListItems = await screen.findAllByTestId("member-item");
    expect(memberListItems).toHaveLength(members.length);

    members.forEach((member) => {
      expect(screen.getByText(member.email)).toBeInTheDocument();
      expect(screen.getByText(member.role)).toBeInTheDocument();
    });
  });

  test("an admin should be able to change the role of a organization member", async () => {
    await setupTestWithUserAndOrg(
      {
        org_id: "1",
        user_id: "1",
        email: "test@example.com",
        role: "admin",
        llm_api_key: "**********",
        max_iterations: 20,
        llm_model: "gpt-4",
        llm_api_key_for_byor: null,
        llm_base_url: "https://api.openai.com",
        status: "active",
      },
      0,
    );

    const updateMemberRoleSpy = createUpdateMemberRoleSpy();

    const memberListItems = await screen.findAllByTestId("member-item");
    const userRoleMember = memberListItems[2]; // third member is "user"

    let userCombobox = within(userRoleMember).getByText(/^Member$/i);
    expect(userCombobox).toBeInTheDocument();

    // Change role from user to admin
    await changeMemberRole(userRoleMember, "member", "admin");

    expect(updateMemberRoleSpy).toHaveBeenCalledExactlyOnceWith({
      userId: "3", // assuming the third member is the one being updated
      orgId: "1",
      role: "admin",
    });
    expectDropdownNotVisible(userRoleMember);

    // Verify the role has been updated in the UI
    userCombobox = within(userRoleMember).getByText(/^Admin$/i);
    expect(userCombobox).toBeInTheDocument();

    // Revert the role back to user
    await changeMemberRole(userRoleMember, "admin", "member");

    expect(updateMemberRoleSpy).toHaveBeenNthCalledWith(2, {
      userId: "3",
      orgId: "1",
      role: "member",
    });

    // Verify the role has been reverted in the UI
    userCombobox = within(userRoleMember).getByText(/^Member$/i);
    expect(userCombobox).toBeInTheDocument();
  });

  it("should not allow an admin to change the owner's role", async () => {
    await verifyRoleChangeNotPermitted(
      {
        org_id: "3",
        user_id: "1",
        email: "test@example.com",
        role: "admin",
        llm_api_key: "**********",
        max_iterations: 20,
        llm_model: "gpt-4",
        llm_api_key_for_byor: null,
        llm_base_url: "https://api.openai.com",
        status: "active",
      },
      2, // user is admin in org 3
      0, // first member is "owner"
      "Owner",
    );
  });

  it("should not allow an admin to change another admin's role", async () => {
    await verifyRoleChangeNotPermitted(
      {
        org_id: "3",
        user_id: "1",
        email: "test@example.com",
        role: "admin",
        llm_api_key: "**********",
        max_iterations: 20,
        llm_model: "gpt-4",
        llm_api_key_for_byor: null,
        llm_base_url: "https://api.openai.com",
        status: "active",
      },
      2, // user is admin in org 3
      1, // second member is "admin"
      "Admin",
    );
  });

  it("should not allow a user to change their own role", async () => {
    // Mock the /me endpoint to return a user ID that matches one of the members
    await verifyRoleChangeNotPermitted(
      {
        org_id: "1",
        user_id: "1", // Same as first member from org 1
        email: "alice@acme.org",
        role: "owner",
        llm_api_key: "**********",
        max_iterations: 20,
        llm_model: "gpt-4",
        llm_api_key_for_byor: null,
        llm_base_url: "https://api.openai.com",
        status: "active",
      },
      0,
      0, // First member (user_id: "1")
      "Owner",
    );
  });

  it("should show a remove option in the role dropdown and remove the user from the list", async () => {
    const removeMemberSpy = vi.spyOn(organizationService, "removeMember");

    await setupTestWithOrg(0);

    // Get initial member count
    const memberListItems = await screen.findAllByTestId("member-item");
    const initialMemberCount = memberListItems.length;

    const userRoleMember = memberListItems[2]; // third member is "user"
    const userEmail = within(userRoleMember).getByText("charlie@acme.org");
    expect(userEmail).toBeInTheDocument();

    const userCombobox = within(userRoleMember).getByText(/^Member$/i);
    await userEvent.click(userCombobox);

    const dropdown = within(userRoleMember).getByTestId(
      "organization-member-role-context-menu",
    );

    // Check that remove option exists
    const removeOption = within(dropdown).getByTestId("remove-option");
    expect(removeOption).toBeInTheDocument();

    await userEvent.click(removeOption);

    expect(removeMemberSpy).toHaveBeenCalledExactlyOnceWith({
      orgId: "1",
      userId: "3",
    });

    // Verify the user is no longer in the list
    await waitFor(() => {
      const updatedMemberListItems = screen.getAllByTestId("member-item");
      expect(updatedMemberListItems).toHaveLength(initialMemberCount - 1);
    });

    // Verify the specific user email is no longer present
    expect(screen.queryByText("charlie@acme.org")).not.toBeInTheDocument();
  });

  it.todo(
    "should not allow a user to change another user's role if they are the same role",
  );

  describe("Inviting Organization Members", () => {
    it("should render an invite organization member button", async () => {
      await setupInviteTest();

      const inviteButton = await findInviteButton();
      expect(inviteButton).toBeInTheDocument();
    });

    it("should render a modal when the invite button is clicked", async () => {
      await setupInviteTest();

      expect(screen.queryByTestId("invite-modal")).not.toBeInTheDocument();
      const inviteButton = await findInviteButton();
      await userEvent.click(inviteButton);

      const portalRoot = screen.getByTestId("portal-root");
      expect(
        within(portalRoot).getByTestId("invite-modal"),
      ).toBeInTheDocument();
    });

    it("should close the modal when the close button is clicked", async () => {
      await setupInviteTest();

      const inviteButton = await findInviteButton();
      await userEvent.click(inviteButton);

      const modal = screen.getByTestId("invite-modal");
      const closeButton = within(modal).getByText("BUTTON$CANCEL");
      await userEvent.click(closeButton);

      expect(screen.queryByTestId("invite-modal")).not.toBeInTheDocument();
    });

    it("should render a list item in an invited state when a the user is is invited", async () => {
      const getOrganizationMembersSpy = vi.spyOn(
        organizationService,
        "getOrganizationMembers",
      );

      getOrganizationMembersSpy.mockResolvedValue([
        {
          org_id: "1",
          user_id: "4",
          email: "tom@acme.org",
          role: "member",
          llm_api_key: "**********",
          max_iterations: 20,
          llm_model: "gpt-4",
          llm_api_key_for_byor: null,
          llm_base_url: "https://api.openai.com",
          status: "invited",
        },
      ]);

      await setupInviteTest();

      const members = await screen.findAllByTestId("member-item");
      expect(members).toHaveLength(1);

      const invitedMember = members[0];
      expect(invitedMember).toBeInTheDocument();

      // should have an "invited" badge
      const invitedBadge = within(invitedMember).getByText(/invited/i);
      expect(invitedBadge).toBeInTheDocument();

      // should not have a role combobox
      await userEvent.click(within(invitedMember).getByText(/^Member$/i));
      expect(
        within(invitedMember).queryByTestId(
          "organization-member-role-context-menu",
        ),
      ).not.toBeInTheDocument();
    });
  });

  describe("Role-based invite permission behavior", () => {
    it.each([
      { role: "owner" as const, roleName: "Owner" },
      { role: "admin" as const, roleName: "Admin" },
    ])(
      "should show invite button when user has canInviteUsers permission ($roleName role)",
      async ({ role }) => {
        getMeSpy.mockResolvedValue({
          org_id: "1",
          user_id: "1",
          email: "test@example.com",
          role,
          llm_api_key: "**********",
          max_iterations: 20,
          llm_model: "gpt-4",
          llm_api_key_for_byor: null,
          llm_base_url: "https://api.openai.com",
          status: "active",
        });

        await setupTestWithOrg(0);

        const inviteButton = await findInviteButton();

        expect(inviteButton).toBeInTheDocument();
        expect(inviteButton).not.toBeDisabled();
      },
    );

    it("should not show invite button when user lacks canInviteUsers permission (User role)", async () => {
      const userData = {
        org_id: "1",
        user_id: "1",
        email: "test@example.com",
        role: "member" as const,
        llm_api_key: "**********",
        max_iterations: 20,
        llm_model: "gpt-4",
        llm_api_key_for_byor: null,
        llm_base_url: "https://api.openai.com",
        status: "active" as const,
      };

      // Set mock and remove cached query before rendering
      getMeSpy.mockResolvedValue(userData);
      // Remove any cached "me" queries so fresh data is fetched
      queryClient.removeQueries({ queryKey: ["organizations"] });

      await setupTestWithOrg(0);

      // Directly set the query data to force component re-render with user role
      // This ensures the component uses the user role data instead of cached admin data
      queryClient.setQueryData(["organizations", "1", "me"], userData);

      // Wait for the component to update with the new query data
      await waitFor(
        () => {
          const inviteButton = screen.queryByRole("button", {
            name: /ORG\$INVITE_ORGANIZATION_MEMBER/i,
          });
          expect(inviteButton).not.toBeInTheDocument();
        },
        { timeout: 3000 },
      );
    });
  });

  describe("Role-based role change permission behavior", () => {
    it("should not allow an owner to change another owner's role", async () => {
      await verifyRoleChangeNotPermitted(
        {
          org_id: "1",
          user_id: "1", // First member is owner in org 1
          email: "alice@acme.org",
          role: "owner",
          llm_api_key: "**********",
          max_iterations: 20,
          llm_model: "gpt-4",
          llm_api_key_for_byor: null,
          llm_base_url: "https://api.openai.com",
          status: "active",
        },
        0,
        0, // First member is owner
        "owner",
      );
    });

    it("Owner should see all three role options (owner, admin, user) in dropdown regardless of target member's role", async () => {
      await setupTestWithUserAndOrg(
        {
          org_id: "1",
          user_id: "1", // First member is owner in org 1
          email: "alice@acme.org",
          role: "owner",
          llm_api_key: "**********",
          max_iterations: 20,
          llm_model: "gpt-4",
          llm_api_key_for_byor: null,
          llm_base_url: "https://api.openai.com",
          status: "active",
        },
        0,
      );

      const memberListItems = await screen.findAllByTestId("member-item");

      // Test with admin member
      const adminMember = memberListItems[1]; // Second member is admin (user_id: "2")
      const adminDropdown = await openRoleDropdown(adminMember, "admin");

      // Verify all three role options are present for admin member
      expectAllRoleOptionsPresent(adminDropdown);

      // Close dropdown by clicking outside
      await closeDropdown();

      // Test with user member
      const userMember = await findMemberByEmail("charlie@acme.org");
      const userDropdown = await openRoleDropdown(userMember, "member");

      // Verify all three role options are present for user member
      expectAllRoleOptionsPresent(userDropdown);
    });

    it("Admin should not see owner option in role dropdown for any member", async () => {
      await setupTestWithUserAndOrg(
        {
          org_id: "3",
          user_id: "7", // Ray is admin in org 3
          email: "ray@all-hands.dev",
          role: "admin",
          llm_api_key: "**********",
          max_iterations: 20,
          llm_model: "gpt-4",
          llm_api_key_for_byor: null,
          llm_base_url: "https://api.openai.com",
          status: "active",
        },
        2, // org 3
      );

      const memberListItems = await screen.findAllByTestId("member-item");

      // Check user member dropdown
      const userMember = memberListItems[2]; // user member
      const userDropdown = await openRoleDropdown(userMember, "member");
      expectOwnerOptionNotPresent(userDropdown);
      await closeDropdown();

      // Check another user member dropdown if exists
      if (memberListItems.length > 3) {
        const anotherUserMember = memberListItems[3]; // another user member
        const anotherUserDropdown = await openRoleDropdown(
          anotherUserMember,
          "member",
        );
        expectOwnerOptionNotPresent(anotherUserDropdown);
      }
    });

    it("Owner should be able to change any member's role to owner", async () => {
      await setupTestWithUserAndOrg(
        {
          org_id: "1",
          user_id: "1", // First member is owner in org 1
          email: "alice@acme.org",
          role: "owner",
          llm_api_key: "**********",
          max_iterations: 20,
          llm_model: "gpt-4",
          llm_api_key_for_byor: null,
          llm_base_url: "https://api.openai.com",
          status: "active",
        },
        0,
      );

      const updateMemberRoleSpy = createUpdateMemberRoleSpy();

      const memberListItems = await screen.findAllByTestId("member-item");

      // Test changing admin to owner
      const adminMember = memberListItems[1]; // Second member is admin (user_id: "2")
      await changeMemberRole(adminMember, "admin", "owner");

      expect(updateMemberRoleSpy).toHaveBeenNthCalledWith(1, {
        userId: "2",
        orgId: "1",
        role: "owner",
      });

      // Test changing user to owner
      const userMember = await findMemberByEmail("charlie@acme.org");
      await changeMemberRole(userMember, "member", "owner");

      expect(updateMemberRoleSpy).toHaveBeenNthCalledWith(2, {
        userId: "3",
        orgId: "1",
        role: "owner",
      });
    });

    it.each([
      {
        description:
          "Owner should be able to change admin's role to admin (no change)",
        userData: {
          org_id: "1",
          user_id: "1", // First member is owner in org 1
          email: "alice@acme.org",
          role: "owner" as const,
          llm_api_key: "**********",
          max_iterations: 20,
          llm_model: "gpt-4",
          llm_api_key_for_byor: null,
          llm_base_url: "https://api.openai.com",
          status: "active" as const,
        },
        orgIndex: 0,
        memberEmail: "bob@acme.org",
        currentRole: "admin",
        newRole: "admin",
        expectedApiCall: {
          userId: "2",
          orgId: "1",
          role: "admin" as const,
        },
      },
      {
        description:
          "Admin should be able to change user's role to user (no change)",
        userData: {
          org_id: "3",
          user_id: "7", // Ray is admin in org 3
          email: "ray@all-hands.dev",
          role: "admin" as const,
          llm_api_key: "**********",
          max_iterations: 20,
          llm_model: "gpt-4",
          llm_api_key_for_byor: null,
          llm_base_url: "https://api.openai.com",
          status: "active" as const,
        },
        orgIndex: 2, // org 3
        memberEmail: "stephan@all-hands.dev",
        currentRole: "member",
        newRole: "member",
        expectedApiCall: {
          userId: "9",
          orgId: "3",
          role: "member" as const,
        },
      },
      {
        description: "Admin should be able to change member's role to admin",
        userData: {
          org_id: "3",
          user_id: "7", // Ray is admin in org 3
          email: "ray@all-hands.dev",
          role: "admin" as const,
          llm_api_key: "**********",
          max_iterations: 20,
          llm_model: "gpt-4",
          llm_api_key_for_byor: null,
          llm_base_url: "https://api.openai.com",
          status: "active" as const,
        },
        orgIndex: 2, // org 3
        memberEmail: "stephan@all-hands.dev",
        currentRole: "member",
        newRole: "admin",
        expectedApiCall: {
          userId: "9",
          orgId: "3",
          role: "admin" as const,
        },
      },
    ])(
      "$description",
      async ({
        userData,
        orgIndex,
        memberEmail,
        currentRole,
        newRole,
        expectedApiCall,
      }) => {
        await setupTestWithUserAndOrg(userData, orgIndex);

        const updateMemberRoleSpy = createUpdateMemberRoleSpy();

        const member = await findMemberByEmail(memberEmail);

        await changeMemberRole(member, currentRole, newRole);

        expect(updateMemberRoleSpy).toHaveBeenCalledExactlyOnceWith(
          expectedApiCall,
        );
      },
    );
  });
});
