defmodule Towerops.OrganizationsTest do
  use Towerops.DataCase

  import Towerops.AccountsFixtures

  alias Towerops.Organizations

  describe "organizations" do
    setup do
      user = user_fixture()
      %{user: user}
    end

    test "list_user_organizations/1 returns all organizations for a user", %{user: user} do
      {:ok, org1} = Organizations.create_organization(%{name: "Org 1"}, user.id)
      {:ok, org2} = Organizations.create_organization(%{name: "Org 2"}, user.id)

      orgs = Organizations.list_user_organizations(user.id)
      assert length(orgs) == 2
      org_ids = Enum.map(orgs, & &1.id)
      assert org1.id in org_ids
      assert org2.id in org_ids
    end

    test "list_user_organizations/1 returns empty list for user with no orgs" do
      user = user_fixture()
      assert Organizations.list_user_organizations(user.id) == []
    end

    test "get_organization!/1 returns the organization with given id", %{user: user} do
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
      assert Organizations.get_organization!(organization.id).id == organization.id
    end

    test "get_organization_by_slug!/1 returns the organization with given slug", %{user: user} do
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
      assert Organizations.get_organization_by_slug!(organization.slug).id == organization.id
    end

    test "create_organization/2 with valid data creates organization", %{user: user} do
      valid_attrs = %{name: "New Organization"}

      assert {:ok, organization} = Organizations.create_organization(valid_attrs, user.id)
      assert organization.name == "New Organization"
      assert organization.slug
    end

    test "create_organization/2 creates owner membership", %{user: user} do
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)

      membership = Organizations.get_membership(organization.id, user.id)
      assert membership.role == :owner
    end

    test "create_organization/2 with invalid data returns error changeset" do
      user = user_fixture()
      assert {:error, changeset} = Organizations.create_organization(%{name: nil}, user.id)
      assert "can't be blank" in errors_on(changeset).name
    end

    test "update_organization/2 with valid data updates the organization", %{user: user} do
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)

      update_attrs = %{name: "Updated Org"}
      assert {:ok, updated} = Organizations.update_organization(organization, update_attrs)
      assert updated.name == "Updated Org"
    end

    test "update_organization/2 with invalid data returns error changeset", %{user: user} do
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)

      assert {:error, changeset} = Organizations.update_organization(organization, %{name: nil})
      assert "can't be blank" in errors_on(changeset).name
    end

    test "delete_organization/1 deletes the organization", %{user: user} do
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
      assert {:ok, _} = Organizations.delete_organization(organization)
      assert_raise Ecto.NoResultsError, fn -> Organizations.get_organization!(organization.id) end
    end

    test "change_organization/1 returns an organization changeset", %{user: user} do
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
      assert %Ecto.Changeset{} = Organizations.change_organization(organization)
    end
  end

  describe "memberships" do
    setup do
      user = user_fixture()
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
      %{user: user, organization: organization}
    end

    test "get_membership/2 returns the membership", %{user: user, organization: organization} do
      membership = Organizations.get_membership(organization.id, user.id)
      assert membership.user_id == user.id
      assert membership.organization_id == organization.id
    end

    test "get_membership/2 returns nil when membership doesn't exist", %{organization: organization} do
      other_user = user_fixture()
      assert Organizations.get_membership(organization.id, other_user.id) == nil
    end

    test "get_membership!/2 returns the membership", %{user: user, organization: organization} do
      membership = Organizations.get_membership!(organization.id, user.id)
      assert membership.user_id == user.id
    end

    test "get_membership!/2 raises when membership doesn't exist", %{organization: organization} do
      other_user = user_fixture()

      assert_raise Ecto.NoResultsError, fn ->
        Organizations.get_membership!(organization.id, other_user.id)
      end
    end

    test "list_organization_memberships/1 returns all memberships", %{
      user: user,
      organization: organization
    } do
      # Add another member
      other_user = user_fixture()

      {:ok, _membership} =
        Organizations.create_membership(%{
          organization_id: organization.id,
          user_id: other_user.id,
          role: :member
        })

      memberships = Organizations.list_organization_memberships(organization.id)
      assert length(memberships) == 2
      user_ids = Enum.map(memberships, & &1.user_id)
      assert user.id in user_ids
      assert other_user.id in user_ids
    end

    test "list_organization_notification_recipients/1 returns owners and admins", %{
      organization: organization
    } do
      admin = user_fixture()
      member = user_fixture()

      {:ok, _admin_membership} =
        Organizations.create_membership(%{
          organization_id: organization.id,
          user_id: admin.id,
          role: :admin
        })

      {:ok, _member_membership} =
        Organizations.create_membership(%{
          organization_id: organization.id,
          user_id: member.id,
          role: :member
        })

      recipients = Organizations.list_organization_notification_recipients(organization.id)
      recipient_ids = Enum.map(recipients, & &1.id)

      # Owner and admin should be in recipients
      assert admin.id in recipient_ids
      # Member should NOT be in recipients
      refute member.id in recipient_ids
    end

    test "create_membership/1 with valid data creates membership", %{organization: organization} do
      new_user = user_fixture()

      valid_attrs = %{
        organization_id: organization.id,
        user_id: new_user.id,
        role: :member
      }

      assert {:ok, membership} = Organizations.create_membership(valid_attrs)
      assert membership.role == :member
      assert membership.user_id == new_user.id
    end

    test "create_membership/1 with invalid data returns error changeset" do
      assert {:error, changeset} = Organizations.create_membership(%{})
      assert "can't be blank" in errors_on(changeset).organization_id
    end

    test "update_membership/2 changes user role", %{user: user, organization: organization} do
      membership = Organizations.get_membership!(organization.id, user.id)

      assert {:ok, updated} = Organizations.update_membership(membership, %{role: :admin})
      assert updated.role == :admin
    end

    test "delete_membership/1 deletes the membership", %{organization: organization} do
      new_user = user_fixture()

      {:ok, membership} =
        Organizations.create_membership(%{
          organization_id: organization.id,
          user_id: new_user.id,
          role: :member
        })

      assert {:ok, _} = Organizations.delete_membership(membership)
      assert Organizations.get_membership(organization.id, new_user.id) == nil
    end
  end

  describe "invitations" do
    setup do
      owner = user_fixture()
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, owner.id)
      %{owner: owner, organization: organization}
    end

    test "list_pending_invitations/1 returns pending invitations", %{
      owner: owner,
      organization: organization
    } do
      {:ok, invitation} =
        Organizations.create_invitation(%{
          organization_id: organization.id,
          email: "invited@example.com",
          role: :member,
          invited_by_id: owner.id,
          token: "test-token-#{System.unique_integer()}",
          expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
        })

      invitations = Organizations.list_pending_invitations(organization.id)
      assert length(invitations) == 1
      assert hd(invitations).id == invitation.id
    end

    test "list_pending_invitations/1 excludes accepted invitations", %{
      owner: owner,
      organization: organization
    } do
      {:ok, invitation} =
        Organizations.create_invitation(%{
          organization_id: organization.id,
          email: "invited@example.com",
          role: :member,
          invited_by_id: owner.id,
          token: "test-token-#{System.unique_integer()}",
          expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second),
          accepted_at: DateTime.truncate(DateTime.utc_now(), :second)
        })

      invitations = Organizations.list_pending_invitations(organization.id)
      refute Enum.any?(invitations, &(&1.id == invitation.id))
    end

    test "list_pending_invitations/1 excludes expired invitations", %{
      owner: owner,
      organization: organization
    } do
      {:ok, invitation} =
        Organizations.create_invitation(%{
          organization_id: organization.id,
          email: "invited@example.com",
          role: :member,
          invited_by_id: owner.id,
          token: "test-token-#{System.unique_integer()}",
          expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), -1, :day), :second)
        })

      invitations = Organizations.list_pending_invitations(organization.id)
      refute Enum.any?(invitations, &(&1.id == invitation.id))
    end

    test "create_invitation/1 with valid data creates invitation", %{
      owner: owner,
      organization: organization
    } do
      valid_attrs = %{
        organization_id: organization.id,
        email: "test@example.com",
        role: :member,
        invited_by_id: owner.id,
        token: "unique-token-#{System.unique_integer()}",
        expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
      }

      assert {:ok, invitation} = Organizations.create_invitation(valid_attrs)
      assert invitation.email == "test@example.com"
      assert invitation.role == :member
    end

    test "get_invitation_by_token/1 returns invitation", %{
      owner: owner,
      organization: organization
    } do
      token = "test-token-#{System.unique_integer()}"

      {:ok, _invitation} =
        Organizations.create_invitation(%{
          organization_id: organization.id,
          email: "test@example.com",
          role: :member,
          invited_by_id: owner.id,
          token: token,
          expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
        })

      invitation = Organizations.get_invitation_by_token(token)
      assert invitation.token == token
    end

    test "get_invitation_by_token/1 returns nil for accepted invitation", %{
      owner: owner,
      organization: organization
    } do
      token = "test-token-#{System.unique_integer()}"

      {:ok, _invitation} =
        Organizations.create_invitation(%{
          organization_id: organization.id,
          email: "test@example.com",
          role: :member,
          invited_by_id: owner.id,
          token: token,
          expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second),
          accepted_at: DateTime.truncate(DateTime.utc_now(), :second)
        })

      assert Organizations.get_invitation_by_token(token) == nil
    end

    test "accept_invitation/2 creates membership and marks invitation accepted", %{
      owner: owner,
      organization: organization
    } do
      token = "test-token-#{System.unique_integer()}"

      {:ok, invitation} =
        Organizations.create_invitation(%{
          organization_id: organization.id,
          email: "test@example.com",
          role: :member,
          invited_by_id: owner.id,
          token: token,
          expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
        })

      new_user = user_fixture()
      assert {:ok, membership} = Organizations.accept_invitation(invitation, new_user.id)
      assert membership.user_id == new_user.id
      assert membership.role == :member

      # Invitation should no longer be retrievable
      assert Organizations.get_invitation_by_token(token) == nil
    end

    test "delete_invitation/1 deletes the invitation", %{
      owner: owner,
      organization: organization
    } do
      {:ok, invitation} =
        Organizations.create_invitation(%{
          organization_id: organization.id,
          email: "test@example.com",
          role: :member,
          invited_by_id: owner.id,
          token: "test-token-#{System.unique_integer()}",
          expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
        })

      assert {:ok, _} = Organizations.delete_invitation(invitation)
      assert Organizations.list_pending_invitations(organization.id) == []
    end
  end

  describe "authorization" do
    setup do
      user = user_fixture()
      {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
      membership = Organizations.get_membership!(organization.id, user.id)
      %{membership: membership}
    end

    test "can?/3 delegates to Policy module", %{membership: membership} do
      # Owner can delete organization
      assert Organizations.can?(membership, :delete, :organization)

      # Change to member role
      {:ok, member_membership} = Organizations.update_membership(membership, %{role: :member})

      # Member cannot delete organization
      refute Organizations.can?(member_membership, :delete, :organization)
    end
  end
end
