defmodule Operately.InviteLinksTest do
  use Operately.DataCase

  import Mock

  alias Operately.InviteLinks
  alias Operately.InviteLinks.InviteLink
  alias Operately.People
  alias Operately.Support.Factory

  setup ctx do
    ctx
    |> Factory.setup()
  end

  describe "invite_links" do
    test "create_invite_link/1 creates a link with valid data", ctx do
      attrs = %{
        company_id: ctx.company.id,
        author_id: ctx.creator.id
      }

      assert {:ok, invite_link} = InviteLinks.create_invite_link(attrs)
      assert invite_link.company_id == ctx.company.id
      assert invite_link.author_id == ctx.creator.id
      assert invite_link.is_active == true
      assert invite_link.use_count == 0
      assert invite_link.token != nil
      assert String.length(invite_link.token) >= 32
      assert invite_link.expires_at != nil
    end

    test "create_invite_link/1 sets expiration to 7 days from creation", ctx do
      attrs = %{
        company_id: ctx.company.id,
        author_id: ctx.creator.id
      }

      {:ok, invite_link} = InviteLinks.create_invite_link(attrs)

      # Check that expires_at is approximately 7 days from now
      seven_days_from_now = DateTime.add(DateTime.utc_now(), 7 * 24 * 60 * 60, :second)
      time_diff = DateTime.diff(invite_link.expires_at, seven_days_from_now, :second)

      # Allow for a few seconds difference due to execution time
      assert abs(time_diff) <= 5
    end

    test "get_invite_link_by_token/1 returns the correct link", ctx do
      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      {:ok, found_link} = InviteLinks.get_invite_link_by_token(invite_link.token)
      assert found_link.id == invite_link.id
    end

    test "get_invite_link_by_token/1 returns nil for invalid token", _ctx do
      assert InviteLinks.get_invite_link_by_token("invalid-token") == {:error, :not_found}
    end

    test "list_invite_links_for_company/1 returns links for the company", ctx do
      # Create a link for the company
      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      # Create another company and link to ensure filtering works
      ctx = Factory.add_company(ctx, :other_company, ctx.account)
      other_creator = Ecto.assoc(ctx.other_company, :people) |> Operately.Repo.all() |> hd()

      {:ok, _other_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.other_company.id,
          author_id: other_creator.id
        })

      links = InviteLinks.list_invite_links_for_company(ctx.company.id)
      assert length(links) == 1
      assert hd(links).id == invite_link.id
    end

    test "revoke_invite_link/1 marks link as inactive", ctx do
      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      assert invite_link.is_active == true

      {:ok, revoked_link} = InviteLinks.revoke_invite_link(invite_link)
      assert revoked_link.is_active == false
    end

    test "increment_use_count/1 increases the count", ctx do
      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      assert invite_link.use_count == 0

      {:ok, updated_link} = InviteLinks.increment_use_count(invite_link)
      assert updated_link.use_count == 1
    end
  end

  describe "invite_link validation" do
    test "is_expired?/1 returns true for expired links", ctx do
      expired_time = DateTime.add(DateTime.utc_now(), -1, :day)

      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id,
          expires_at: expired_time
        })

      assert InviteLinks.InviteLink.is_expired?(invite_link) == true
    end

    test "is_expired?/1 returns false for non-expired links", ctx do
      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      assert InviteLinks.InviteLink.is_expired?(invite_link) == false
    end

    test "is_valid?/1 returns false for inactive links", ctx do
      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      {:ok, revoked_link} = InviteLinks.revoke_invite_link(invite_link)
      assert InviteLinks.InviteLink.is_valid?(revoked_link) == false
    end

    test "is_valid?/1 returns false for expired links", ctx do
      expired_time = DateTime.add(DateTime.utc_now(), -1, :day)

      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id,
          expires_at: expired_time
        })

      assert InviteLinks.InviteLink.is_valid?(invite_link) == false
    end

    test "is_valid?/1 returns true for active, non-expired links", ctx do
      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      assert InviteLinks.InviteLink.is_valid?(invite_link) == true
    end
  end

  describe "join_company_via_invite_link/2" do
    test "creates a new person for a valid invite", ctx do
      ctx = Factory.add_account(ctx, :new_account)

      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      assert {:ok, person} = InviteLinks.join_company_via_invite_link(ctx.new_account, invite_link.token)
      assert person.account_id == ctx.new_account.id
      assert person.company_id == ctx.company.id

      reloaded_link = Repo.get!(InviteLink, invite_link.id)
      assert reloaded_link.use_count == 1

      assert People.get_person(ctx.new_account, ctx.company).id == person.id
    end

    test "returns existing person when the account is already in the company", ctx do
      {:ok, invite_link} = InviteLinks.create_invite_link(%{company_id: ctx.company.id, author_id: ctx.creator.id})

      existing_person = People.get_person(ctx.account, ctx.company)
      refute is_nil(existing_person)

      assert {:ok, person} = InviteLinks.join_company_via_invite_link(ctx.account, invite_link.token)
      assert person.id == existing_person.id

      reloaded_link = Repo.get!(InviteLink, invite_link.id)
      assert reloaded_link.use_count == 0
    end

    test "returns error when invite token cannot be found", ctx do
      assert InviteLinks.join_company_via_invite_link(ctx.account, "missing-token") == {:error, :invite_token_not_found}
    end

    test "returns error when the invite link is inactive", ctx do
      ctx = Factory.add_account(ctx, :new_account)

      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      {:ok, revoked_link} = InviteLinks.revoke_invite_link(invite_link)

      assert InviteLinks.join_company_via_invite_link(ctx.new_account, revoked_link.token) == {:error, :invite_token_inactive}
    end

    test "returns error when the invite link is expired", ctx do
      ctx = Factory.add_account(ctx, :new_account)

      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id,
          expires_at: DateTime.add(DateTime.utc_now(), -60, :second)
        })

      assert InviteLinks.join_company_via_invite_link(ctx.new_account, invite_link.token) == {:error, :invite_token_expired}
    end

    test "returns error when person creation fails", ctx do
      ctx = Factory.add_account(ctx, :new_account)

      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      changeset = Operately.People.Person.changeset(%Operately.People.Person{}, %{})

      with_mock Operately.People, [:passthrough], create_person: fn _attrs -> {:error, changeset} end do
        assert InviteLinks.join_company_via_invite_link(ctx.new_account, invite_link.token) == {:error, :person_creation_failed}
        assert_called(Operately.People.create_person(:_))
      end

      reloaded_link = Repo.get!(InviteLink, invite_link.id)
      assert reloaded_link.use_count == 0
    end

    test "returns error when updating the invite link fails", ctx do
      ctx = Factory.add_account(ctx, :new_account)

      {:ok, invite_link} =
        InviteLinks.create_invite_link(%{
          company_id: ctx.company.id,
          author_id: ctx.creator.id
        })

      with_mock Operately.InviteLinks, [:passthrough], increment_use_count: fn _invite_link -> {:error, :db_error} end do
        assert InviteLinks.join_company_via_invite_link(ctx.new_account, invite_link.token) == {:error, :invite_link_update_failed}
        assert_called(Operately.InviteLinks.increment_use_count(:_))
      end

      reloaded_link = Repo.get!(InviteLink, invite_link.id)
      assert reloaded_link.use_count == 0
    end
  end
end
