defmodule Plausible.SitesTest do
  use Plausible.DataCase
  use Plausible
  use Plausible.Teams.Test

  alias Plausible.Sites

  describe "create a site" do
    test "creates a site" do
      user = new_user()

      params = %{"domain" => "example.com", "timezone" => "Europe/London"}

      assert {:ok, %{site: %{domain: "example.com", timezone: "Europe/London"}}} =
               Sites.create(user, params)
    end

    test "creating a site sets `legacy_time_on_page_cutoff`" do
      user = new_user()

      params = %{"domain" => "example.com", "timezone" => "Europe/London"}

      assert {:ok, %{site: %{legacy_time_on_page_cutoff: ~D[1970-01-01]}}} =
               Sites.create(user, params)
    end

    @tag :ee_only
    test "updates team's locked state" do
      user = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1), team: [locked: false])

      team = new_site(owner: user).team

      refute team.locked

      params = %{"domain" => "example.com", "timezone" => "Europe/London"}

      assert {:ok, %{site: %{domain: "example.com", timezone: "Europe/London"}}} =
               Sites.create(user, params, team)

      assert Repo.reload(team).locked
    end

    test "does not start a trial for pre-teams guest users without trial expiry date" do
      user = new_user() |> subscribe_to_growth_plan()
      new_site(owner: user)

      three_hundred_days_from_now = Date.shift(Date.utc_today(), day: 300)

      user
      |> team_of()
      |> Ecto.Changeset.change(
        trial_expiry_date: nil,
        accept_traffic_until: three_hundred_days_from_now
      )
      |> Plausible.Repo.update!()

      params = %{"domain" => "example.com", "timezone" => "Europe/London"}

      user = Plausible.Repo.reload!(user)

      assert {:ok, %{site: %{domain: "example.com", timezone: "Europe/London"}}} =
               Sites.create(user, params)

      team = user |> team_of() |> Repo.reload!()
      refute team.trial_expiry_date
      assert Date.compare(team.accept_traffic_until, three_hundred_days_from_now) == :eq
    end

    test "fails on invalid timezone" do
      user = insert(:user)

      params = %{"domain" => "example.com", "timezone" => "blah"}

      assert {:error, :site, %{errors: [timezone: {"is invalid", []}]}, %{}} =
               Sites.create(user, params)
    end

    test "fails for user owning more than one team without explicit pick" do
      user = new_user()
      _site1 = new_site(owner: user)
      site2 = new_site()
      add_member(site2.team, user: user, role: :owner)

      params = %{"domain" => "example.com", "timezone" => "Europe/London"}

      assert {:error, _, :multiple_teams, _} = Sites.create(user, params)
    end

    test "fails for user not being permitted to add sites in selected team" do
      user = new_user()
      site = new_site()
      viewer_team = site.team
      add_member(viewer_team, user: user, role: :viewer)
      other_site = new_site()
      other_team = other_site.team

      params = %{"domain" => "example.com", "timezone" => "Europe/London"}

      assert {:error, _, :permission_denied, _} = Sites.create(user, params, viewer_team)
      assert {:error, _, :permission_denied, _} = Sites.create(user, params, other_team)
    end

    test "succeeds for user being permitted to add sites in selected team" do
      user = new_user()
      viewer_site = new_site()
      viewer_team = viewer_site.team
      editor_site = new_site()
      editor_team = editor_site.team

      add_member(viewer_team, user: user, role: :viewer)
      add_member(editor_team, user: user, role: :editor)

      params = %{"domain" => "example.com", "timezone" => "Europe/London"}

      assert {:ok, %{site: site}} = Sites.create(user, params, editor_team)

      assert site.team_id == editor_team.id
    end
  end

  on_ee do
    describe "create a site - SSO user" do
      setup [:create_user, :create_team, :create_site, :setup_sso, :provision_sso_user]

      test "creates a site for SSO user in a setup team", %{user: user, team: team} do
        params = %{"domain" => "example.com", "timezone" => "Europe/London"}

        assert {:ok, %{site: %{domain: "example.com", timezone: "Europe/London"}}} =
                 Sites.create(user, params, team)
      end

      test "does not allow creating a site in SSO user's personal team", %{
        team: team,
        sso_integration: integration
      } do
        user = add_member(team, role: :editor)
        {:ok, personal_team} = Plausible.Teams.get_or_create(user)
        identity = new_identity(user.name, user.email, integration)
        {:ok, _, _, user} = Plausible.Auth.SSO.provision_user(identity)

        params = %{"domain" => "example.com", "timezone" => "Europe/London"}

        assert {:error, _, :permission_denied, _} = Sites.create(user, params, personal_team)
      end
    end
  end

  describe "stats_start_date" do
    test "is nil if site has no stats" do
      site = insert(:site)

      assert Sites.stats_start_date(site) == nil
    end

    test "is date if site does have stats" do
      site = insert(:site)

      populate_stats(site, [
        build(:pageview)
      ])

      assert Sites.stats_start_date(site) == Plausible.Times.today(site.timezone)
    end

    test "memoizes value of start date" do
      site = insert(:site)

      assert site.stats_start_date == nil

      populate_stats(site, [
        build(:pageview)
      ])

      assert Sites.stats_start_date(site) == Plausible.Times.today(site.timezone)
      assert Repo.reload!(site).stats_start_date == Plausible.Times.today(site.timezone)
    end

    on_ee do
      alias Plausible.ConsolidatedView

      test "resets consolidated view stats dates every time" do
        owner = new_user()
        new_site(owner: owner)
        team = team_of(owner)

        {:ok, consolidated_view} = ConsolidatedView.enable(team)

        assert consolidated_view.stats_start_date == ~D[2000-01-01]
        assert Sites.stats_start_date(consolidated_view) == ~D[2000-01-01]

        new_site(owner: owner, native_stats_start_at: ~N[1999-01-01 12:00:00])

        assert Sites.stats_start_date(consolidated_view) == ~D[1999-01-01]
      end
    end
  end

  describe "native_stats_start_date" do
    test "is nil if site has no stats" do
      site = insert(:site)

      assert Sites.native_stats_start_date(site) == nil
    end

    test "is date if site does have stats" do
      site = insert(:site)

      populate_stats(site, [
        build(:pageview)
      ])

      assert Sites.native_stats_start_date(site) == Plausible.Times.today(site.timezone)
    end

    test "ignores imported stats" do
      site = insert(:site)
      insert(:site_import, site: site)

      assert Sites.native_stats_start_date(site) == nil
    end
  end

  describe "has_stats?" do
    test "is false if site has no stats" do
      site = insert(:site)

      refute Sites.has_stats?(site)
    end

    test "is true if site has stats" do
      site = insert(:site)

      populate_stats(site, [
        build(:pageview)
      ])

      assert Sites.has_stats?(site)
    end
  end

  describe "get_for_user/2" do
    @tag :ee_only
    test "get site for super_admin" do
      user1 = new_user()
      user2 = new_user()
      patch_env(:super_admin_user_ids, [user2.id])

      %{id: site_id, domain: domain} = new_site(owner: user1)
      assert %{id: ^site_id} = Sites.get_for_user(user1, domain)

      assert %{id: ^site_id} =
               Sites.get_for_user(user1, domain, roles: [:owner])

      assert is_nil(Sites.get_for_user(user2, domain))

      assert %{id: ^site_id} =
               Sites.get_for_user(user2, domain, roles: [:super_admin])
    end

    test "ignores consolidated site by default" do
      user = new_user()
      %{domain: domain} = new_site(owner: user, consolidated: true)
      refute Sites.get_for_user(user, domain)
      assert_raise(Ecto.NoResultsError, fn -> Sites.get_for_user!(user, domain) end)
    end

    test "includes consolidated site when explicitly requested" do
      user = new_user()
      %{domain: domain} = new_site(owner: user, consolidated: true)
      assert Sites.get_for_user(user, domain, include_consolidated?: true)
      assert Sites.get_for_user!(user, domain, include_consolidated?: true)
    end
  end

  describe "list/3 and list_with_invitations/3" do
    test "returns empty when there are no sites" do
      user = new_user()
      _rogue_site = new_site()

      assert %{
               entries: [],
               page_size: 24,
               page_number: 1,
               total_entries: 0,
               total_pages: 1
             } = Sites.list(user, %{})

      assert %{
               entries: [],
               page_size: 24,
               page_number: 1,
               total_entries: 0,
               total_pages: 1
             } = Plausible.Teams.Sites.list(user, %{})

      assert %{
               entries: [],
               page_size: 24,
               page_number: 1,
               total_entries: 0,
               total_pages: 1
             } = Sites.list_with_invitations(user, %{})

      assert %{
               entries: [],
               page_size: 24,
               page_number: 1,
               total_entries: 0,
               total_pages: 1
             } = Plausible.Teams.Sites.list_with_invitations(user, %{})
    end

    test "lists guest sites, site invitations and transfers when no current team set" do
      user1 = new_user()
      user2 = new_user()
      user3 = new_user()
      user4 = new_user()

      # owned site on a setup team
      site1 = new_site(owner: user1, domain: "own.example.com")

      Plausible.Teams.complete_setup(site1.team)

      # excluded 
      new_site(owner: user1, domain: "consolidated.example.com", consolidated: true)

      # guest site access
      site2 = new_site(owner: user2, domain: "guest.example.com")
      add_guest(site2, user: user1, role: :editor)

      # site invitation
      site3 = new_site(owner: user3, domain: "invitation.example.com")
      invite_guest(site3, user1, role: :viewer, inviter: user3)

      # transfer
      site4 = new_site(domain: "transfer.example.com", owner: user3)
      invite_transfer(site4, user1, inviter: user2)

      # other team site access
      site5 = new_site(domain: "team.example.com", owner: user4)
      add_member(site5.team, user: user1, role: :editor)

      assert %{
               entries: [
                 %{domain: "invitation.example.com"},
                 %{domain: "transfer.example.com"},
                 %{domain: "guest.example.com"}
               ]
             } =
               Sites.list_with_invitations(user1, %{})
    end

    test "lists guest sites, site invitations, transfers and team sites when current team set but not setup" do
      user1 = new_user()
      user2 = new_user()
      user3 = new_user()
      user4 = new_user()

      # owned site on a personal team
      site1 = new_site(owner: user1, domain: "own.example.com")

      # guest site access
      site2 = new_site(owner: user2, domain: "guest.example.com")
      add_guest(site2, user: user1, role: :editor)

      # site invitation
      site3 = new_site(owner: user3, domain: "invitation.example.com")
      invite_guest(site3, user1, role: :viewer, inviter: user3)

      # transfer
      site4 = new_site(domain: "transfer.example.com", owner: user3)
      invite_transfer(site4, user1, inviter: user2)

      # other team site access
      site5 = new_site(domain: "team.example.com", owner: user4)
      add_member(site5.team, user: user1, role: :editor)

      # excluded 
      new_site(owner: user1, domain: "consolidated.example.com", consolidated: true)

      assert %{
               entries: [
                 %{domain: "invitation.example.com"},
                 %{domain: "transfer.example.com"},
                 %{domain: "guest.example.com"},
                 %{domain: "own.example.com"}
               ]
             } =
               Sites.list_with_invitations(user1, %{}, team: site1.team)
    end

    test "lists team sites and transfers when current team set and setup" do
      user1 = new_user()
      user2 = new_user()
      user3 = new_user()
      user4 = new_user()

      # owned site on a personal team
      new_site(owner: user1, domain: "own.example.com")

      # guest site access
      site2 = new_site(owner: user2, domain: "guest.example.com")
      add_guest(site2, user: user1, role: :editor)

      # site invitation
      site3 = new_site(owner: user3, domain: "invitation.example.com")
      invite_guest(site3, user1, role: :viewer, inviter: user3)

      # transfer
      site4 = new_site(domain: "transfer.example.com", owner: user3)
      invite_transfer(site4, user1, inviter: user2)

      # other team site access
      site5 = new_site(domain: "team.example.com", owner: user4)
      team5 = Plausible.Teams.complete_setup(site5.team)
      add_member(site5.team, user: user1, role: :admin)

      # excluded 
      new_site(owner: user1, domain: "consolidated.example.com", consolidated: true)

      assert %{
               entries: [
                 %{domain: "transfer.example.com"},
                 %{domain: "team.example.com"}
               ]
             } =
               Sites.list_with_invitations(user1, %{}, team: team5)
    end

    test "shows both pending transfer and pinned site for user without team with guest membership" do
      owner = new_user()
      pending_owner = new_user()
      site = new_site(owner: owner, domain: "one.example.com")
      add_guest(site, user: pending_owner, role: :editor)

      invite_transfer(site, pending_owner, inviter: owner)

      {:ok, _} = Sites.toggle_pin(pending_owner, site)

      # excluded 
      new_site(owner: owner, domain: "consolidated.example.com", consolidated: true)
      new_site(owner: pending_owner, domain: "consolidated2.example.com", consolidated: true)

      assert %{
               entries: [
                 %{domain: "one.example.com", entry_type: "invitation"},
                 %{domain: "one.example.com", entry_type: "pinned_site"}
               ]
             } =
               Sites.list_with_invitations(pending_owner, %{})
    end

    test "shows both pending transfer and site for user without team with guest membership" do
      owner = new_user()
      pending_owner = new_user()
      site = new_site(owner: owner, domain: "one.example.com")
      add_guest(site, user: pending_owner, role: :editor)

      invite_transfer(site, pending_owner, inviter: owner)

      assert %{
               entries: [
                 %{domain: "one.example.com", entry_type: "invitation"},
                 %{domain: "one.example.com", entry_type: "site"}
               ]
             } =
               Sites.list_with_invitations(pending_owner, %{})
    end

    test "shows both pending transfer and site for user with personal team with guest membership" do
      owner = new_user()
      pending_owner = new_user() |> subscribe_to_growth_plan()
      pending_team = team_of(pending_owner)
      site = new_site(owner: owner, domain: "one.example.com")
      add_guest(site, user: pending_owner, role: :editor)

      invite_transfer(site, pending_owner, inviter: owner)

      assert %{
               entries: [
                 %{domain: "one.example.com", entry_type: "invitation"},
                 %{domain: "one.example.com", entry_type: "site"}
               ]
             } =
               Sites.list_with_invitations(pending_owner, %{}, team: pending_team)
    end

    test "shows only pending transfer for user with setup team with guest membership" do
      owner = new_user()
      pending_owner = new_user() |> subscribe_to_growth_plan()

      pending_team =
        pending_owner
        |> team_of()
        |> Plausible.Teams.complete_setup()

      site = new_site(owner: owner, domain: "one.example.com")
      add_guest(site, user: pending_owner, role: :editor)

      invite_transfer(site, pending_owner, inviter: owner)

      assert %{
               entries: [
                 %{domain: "one.example.com", entry_type: "invitation"}
               ]
             } =
               Sites.list_with_invitations(pending_owner, %{}, team: pending_team)
    end

    test "does not show transfer for user with site in their personal team" do
      owner = new_user()
      pending_owner = new_user() |> subscribe_to_growth_plan()
      pending_team = team_of(pending_owner)
      site = new_site(owner: pending_owner, domain: "one.example.com")

      invite_transfer(site, pending_owner, inviter: owner)

      assert %{
               entries: [
                 %{domain: "one.example.com", entry_type: "site"}
               ]
             } =
               Sites.list_with_invitations(pending_owner, %{}, team: pending_team)
    end

    test "does not show transfer for user with site in their setup team" do
      owner = new_user()
      pending_owner = new_user() |> subscribe_to_growth_plan()
      pending_team = team_of(pending_owner)
      site = new_site(owner: pending_owner, domain: "one.example.com")

      pending_team = Plausible.Teams.complete_setup(pending_team)

      invite_transfer(site, pending_owner, inviter: owner)

      assert %{
               entries: [
                 %{domain: "one.example.com", entry_type: "site"}
               ]
             } =
               Sites.list_with_invitations(pending_owner, %{}, team: pending_team)
    end

    test "pinned site doesn't matter with membership revoked (no active invitations)" do
      user1 = new_user(email: "user1@example.com")
      _user2 = new_user(email: "user2@example.com")

      new_site(owner: user1, domain: "one.example.com")
      site2 = new_site(domain: "two.example.com")

      user1 = site2 |> add_guest(user: user1, role: :viewer)

      {:ok, _} = Sites.toggle_pin(user1, site2)

      revoke_membership(site2, user1)

      assert %{entries: [%{domain: "one.example.com"}]} = Sites.list(user1, %{})
      assert %{entries: [%{domain: "one.example.com"}]} = Sites.list_with_invitations(user1, %{})

      assert %{entries: [%{domain: "one.example.com"}]} = Plausible.Teams.Sites.list(user1, %{})

      assert %{entries: [%{domain: "one.example.com"}]} =
               Plausible.Teams.Sites.list_with_invitations(user1, %{})
    end

    test "pinned site with active invitation" do
      user1 = new_user(email: "user1@example.com")
      user2 = new_user(email: "user2@example.com")

      site1 = new_site(domain: "one.example.com", owner: user1)
      site2 = new_site(domain: "two.example.com")

      invite_guest(site2, user1, role: :editor, inviter: user2)

      {:ok, _} = Sites.toggle_pin(user1, site1)

      assert %{entries: [%{domain: "one.example.com"}]} = Sites.list(user1, %{})

      assert %{
               entries: [
                 %{domain: "two.example.com", entry_type: "invitation"},
                 %{domain: "one.example.com", entry_type: "pinned_site"}
               ]
             } =
               Sites.list_with_invitations(user1, %{})

      assert %{entries: [%{domain: "one.example.com"}]} = Plausible.Teams.Sites.list(user1, %{})

      assert %{entries: [%{domain: "two.example.com"}, %{domain: "one.example.com"}]} =
               Plausible.Teams.Sites.list_with_invitations(user1, %{})
    end

    test "pinned site on active invitation" do
      user1 = new_user(email: "user1@example.com")
      user2 = new_user(email: "user2@example.com")

      site1 = new_site(domain: "one.example.com", owner: user2)

      add_guest(site1, user: user1, role: :editor)
      {:ok, _} = Sites.toggle_pin(user1, site1)
      revoke_membership(site1, user1)

      invite_guest(site1, user1, role: :editor, inviter: user2)

      assert %{entries: []} = Sites.list(user1, %{})

      assert %{
               entries: [
                 %{domain: "one.example.com", entry_type: "invitation"}
               ]
             } =
               Sites.list_with_invitations(user1, %{})

      assert %{entries: []} = Plausible.Teams.Sites.list(user1, %{})

      assert %{entries: [%{domain: "one.example.com", entry_type: "invitation"}]} =
               Plausible.Teams.Sites.list_with_invitations(user1, %{})
    end

    test "puts invitations first, pinned sites second, sites last" do
      user1 = new_user()
      user2 = new_user()
      user3 = new_user()

      site1 = new_site(owner: user1, domain: "one.example.com")
      site2 = new_site(owner: user2, domain: "two.example.com")
      site3 = new_site(owner: user3, domain: "three.example.com")
      site4 = new_site(domain: "four.example.com")
      site5 = new_site(owner: user3, domain: "five.example.com")

      # excluded
      new_site(owner: user1, domain: "consolidated1.example.com", consolidated: true)
      new_site(owner: user2, domain: "consolidated2.example.com", consolidated: true)
      new_site(owner: user3, domain: "consolidated3.example.com", consolidated: true)

      invite_guest(site2, user1, role: :editor, inviter: user2)
      add_guest(site3, user: user1, role: :viewer)
      add_guest(site4, user: user1, role: :editor)

      invite_transfer(site5, user1, inviter: user3)

      {:ok, _} = Sites.toggle_pin(user1, site3)
      {:ok, _pin_to_ignore} = Sites.toggle_pin(user2, site2)

      site1_id = site1.id
      site2_id = site2.id
      site3_id = site3.id
      site4_id = site4.id
      site5_id = site5.id

      assert %{
               entries: [
                 %{id: ^site3_id, entry_type: "pinned_site"},
                 %{id: ^site4_id, entry_type: "site"},
                 %{id: ^site1_id, entry_type: "site"}
               ]
             } = Sites.list(user1, %{})

      assert %{
               entries: [
                 %{id: ^site5_id, entry_type: "invitation"},
                 %{id: ^site2_id, entry_type: "invitation"},
                 %{id: ^site3_id, entry_type: "pinned_site"},
                 %{id: ^site4_id, entry_type: "site"},
                 %{id: ^site1_id, entry_type: "site"}
               ]
             } = Sites.list_with_invitations(user1, %{})
    end

    test "pinned sites are ordered according to the time they were pinned at" do
      user1 = new_user()
      user2 = new_user()
      user3 = new_user()

      site1 = new_site(owner: user1, domain: "one.example.com")
      site2 = new_site(owner: user2, domain: "two.example.com")
      site3 = new_site(domain: "three.example.com")
      site4 = new_site(domain: "four.example.com")
      site5 = new_site(owner: user3, domain: "five.example.com")

      invite_guest(site2, user1, role: :editor, inviter: user2)
      add_guest(site3, user: user1, role: :viewer)
      add_guest(site4, user: user1, role: :editor)

      invite_transfer(site5, user1, inviter: user3)

      {:ok, _} = Sites.toggle_pin(user1, site3)

      site1_id = site1.id
      site2_id = site2.id
      site3_id = site3.id
      site4_id = site4.id
      site5_id = site5.id

      Sites.set_option(user1, site1, :pinned_at, ~N[2023-10-22 12:00:00])
      {:ok, _} = Sites.toggle_pin(user1, site3)

      assert %{
               entries: [
                 %{id: ^site3_id, entry_type: "pinned_site"},
                 %{id: ^site1_id, entry_type: "pinned_site"},
                 %{id: ^site4_id, entry_type: "site"}
               ]
             } = Sites.list(user1, %{})

      assert %{
               entries: [
                 %{id: ^site5_id, entry_type: "invitation"},
                 %{id: ^site2_id, entry_type: "invitation"},
                 %{id: ^site3_id, entry_type: "pinned_site"},
                 %{id: ^site1_id, entry_type: "pinned_site"},
                 %{id: ^site4_id, entry_type: "site"}
               ]
             } = Sites.list_with_invitations(user1, %{})
    end

    test "filters by domain" do
      user1 = new_user()
      user2 = new_user()
      user3 = new_user()

      site1 = new_site(owner: user1, domain: "first.example.com")
      site2 = new_site(owner: user2, domain: "first-transfer.example.com")
      site3 = new_site(owner: user3, domain: "first-invitation.example.com")
      _site4 = new_site(owner: user1, domain: "another.example.com")
      new_site(owner: user1, domain: "consolidated.example.com", consolidated: true)

      invite_guest(site3, user1, role: :viewer, inviter: user3)
      invite_transfer(site2, user1, inviter: user2)

      site1_id = site1.id
      site2_id = site2.id
      site3_id = site3.id

      assert %{
               entries: [
                 %{id: ^site1_id}
               ]
             } = Sites.list(user1, %{}, filter_by_domain: "first")

      assert %{
               entries: [
                 %{id: ^site3_id},
                 %{id: ^site2_id},
                 %{id: ^site1_id}
               ]
             } = Sites.list_with_invitations(user1, %{}, filter_by_domain: "first")
    end

    test "scopes by team when provided" do
      user1 = new_user()
      user2 = new_user()
      user3 = new_user()

      site1 = new_site(owner: user1, domain: "first.example.com")
      site2 = new_site(owner: user2, domain: "first-transfer.example.com")
      site3 = new_site(owner: user3, domain: "first-invitation.example.com")
      site4 = new_site(domain: "zzzsitefromanotherteam.com")

      invite_guest(site3, user1, role: :viewer, inviter: user3)
      invite_transfer(site2, user1, inviter: user2)
      team4 = Plausible.Teams.complete_setup(site4.team)
      add_member(team4, user: user1, role: :admin)

      assert_matches %{
                       entries: [
                         %{id: ^site1.id}
                       ]
                     } = Sites.list(user1, %{})

      assert_matches %{
                       entries: [
                         %{id: ^site3.id},
                         %{id: ^site2.id},
                         %{id: ^site1.id}
                       ]
                     } = Sites.list_with_invitations(user1, %{})

      assert_matches %{
                       entries: [
                         %{id: ^site4.id}
                       ]
                     } = Sites.list(user1, %{}, team: team4)

      assert_matches %{
                       entries: [
                         %{id: ^site2.id},
                         %{id: ^site4.id}
                       ]
                     } = Sites.list_with_invitations(user1, %{}, team: team4)
    end

    test "handles pagination correctly" do
      user1 = new_user()
      user2 = new_user()
      user3 = new_user()

      site1 = new_site(owner: user1, domain: "one.example.com")
      site2 = new_site(owner: user2, domain: "two.example.com")
      site3 = new_site(domain: "three.example.com")
      site4 = new_site(domain: "four.example.com")
      site5 = new_site(owner: user3, domain: "five.example.com")

      invite_guest(site2, user1, role: :editor, inviter: user2)
      add_guest(site3, user: user1, role: :viewer)
      add_guest(site4, user: user1, role: :editor)

      invite_transfer(site5, user1, inviter: user3)

      {:ok, _} = Sites.toggle_pin(user1, site3)

      site1_id = site1.id
      site2_id = site2.id
      site3_id = site3.id
      site4_id = site4.id
      site5_id = site5.id

      assert %{
               entries: [%{id: ^site3_id}, %{id: ^site4_id}],
               page_number: 1,
               page_size: 2,
               total_entries: 3,
               total_pages: 2
             } = Sites.list(user1, %{"page_size" => 2})

      assert %{
               entries: [%{id: ^site1_id}],
               page_number: 2,
               page_size: 2,
               total_entries: 3,
               total_pages: 2
             } = Sites.list(user1, %{"page_size" => 2, "page" => 2})

      assert %{
               entries: [%{id: ^site3_id}, %{id: ^site4_id}, %{id: ^site1_id}],
               page_number: 1,
               page_size: 3,
               total_entries: 3,
               total_pages: 1
             } = Sites.list(user1, %{"page_size" => 3})

      # list_with_invitations
      #
      assert %{
               entries: [%{id: ^site5_id}, %{id: ^site2_id}],
               page_number: 1,
               page_size: 2,
               total_entries: 5,
               total_pages: 3
             } = Sites.list_with_invitations(user1, %{"page_size" => 2})

      assert %{
               entries: [%{id: ^site3_id}, %{id: ^site4_id}],
               page_number: 2,
               page_size: 2,
               total_entries: 5,
               total_pages: 3
             } = Sites.list_with_invitations(user1, %{"page_size" => 2, "page" => 2})

      assert %{
               entries: [%{id: ^site1_id}],
               page_number: 3,
               page_size: 2,
               total_entries: 5,
               total_pages: 3
             } = Sites.list_with_invitations(user1, %{"page_size" => 2, "page" => 3})
    end
  end

  describe "set_option/4" do
    test "allows setting option multiple times" do
      user = new_user()
      site = new_site(owner: user)

      assert prefs =
               %{pinned_at: %NaiveDateTime{}} =
               Sites.set_option(user, site, :pinned_at, NaiveDateTime.utc_now())

      prefs = Repo.reload!(prefs)
      assert prefs.site_id == site.id
      assert prefs.user_id == user.id
      assert prefs.pinned_at

      assert prefs =
               %{pinned_at: nil} = Sites.set_option(user, site, :pinned_at, nil)

      prefs = Repo.reload!(prefs)
      assert prefs.site_id == site.id
      assert prefs.user_id == user.id
      refute prefs.pinned_at

      assert prefs =
               %{pinned_at: %NaiveDateTime{}} =
               Sites.set_option(user, site, :pinned_at, NaiveDateTime.utc_now())

      prefs = Repo.reload!(prefs)
      assert prefs.site_id == site.id
      assert prefs.user_id == user.id
      assert prefs.pinned_at
    end

    test "raises on invalid option" do
      user = new_user()
      site = new_site(owner: user)

      assert_raise FunctionClauseError, fn ->
        Sites.set_option(user, site, :invalid, false)
      end
    end

    test "raises on invalid site/user combination" do
      user = insert(:user)
      site = insert(:site)

      assert_raise Ecto.NoResultsError, fn ->
        Sites.set_option(user, site, :pinned_at, nil)
      end
    end
  end

  describe "toggle_pin/2" do
    test "allows pinning and unpinning site" do
      user = new_user()
      site = new_site(owner: user)

      site = %{site | pinned_at: nil}
      assert {:ok, prefs} = Sites.toggle_pin(user, site)
      assert prefs = %{pinned_at: %NaiveDateTime{}} = prefs
      prefs = Repo.reload!(prefs)
      assert prefs.site_id == site.id
      assert prefs.user_id == user.id
      assert prefs.pinned_at

      site = %{site | pinned_at: NaiveDateTime.utc_now()}
      assert {:ok, prefs} = Sites.toggle_pin(user, site)
      assert %{pinned_at: nil} = prefs
      prefs = Repo.reload!(prefs)
      assert prefs.site_id == site.id
      assert prefs.user_id == user.id
      refute prefs.pinned_at

      site = %{site | pinned_at: nil}
      assert {:ok, prefs} = Sites.toggle_pin(user, site)
      assert %{pinned_at: %NaiveDateTime{}} = prefs
      prefs = Repo.reload!(prefs)
      assert prefs.site_id == site.id
      assert prefs.user_id == user.id
      assert prefs.pinned_at
    end

    test "handles multiple guest memberships with same team properly (regression)" do
      user = new_user()
      owner = new_user()
      site1 = new_site(owner: owner)
      site2 = new_site(owner: owner)
      add_guest(site1, user: user, role: :viewer)
      add_guest(site2, user: user, role: :viewer)

      assert {:ok, prefs} = Sites.toggle_pin(user, site1)
      assert prefs.site_id == site1.id
      assert prefs.user_id == user.id
      assert prefs.pinned_at
    end

    test "returns error when pins limit hit" do
      user = new_user()

      for _ <- 1..9 do
        site = new_site(owner: user)
        assert {:ok, _} = Sites.toggle_pin(user, site)
      end

      site = new_site(owner: user)

      assert {:error, :too_many_pins} = Sites.toggle_pin(user, site)
    end
  end

  describe "get_by_domain/2" do
    test "returns site when found with regular site" do
      site = new_site(domain: "example.com")

      assert %{domain: "example.com"} = Sites.get_by_domain("example.com")
      assert site.id == Sites.get_by_domain("example.com").id
    end

    test "returns nil when site not found" do
      refute Sites.get_by_domain("nonexistent.com")
    end

    test "excludes consolidated sites by default" do
      _regular_site = new_site(domain: "regular.com")
      _consolidated_site = new_site(domain: "consolidated.com", consolidated: true)

      assert Sites.get_by_domain("regular.com")
      refute Sites.get_by_domain("consolidated.com")
    end

    test "includes consolidated sites when explicitly requested" do
      _regular_site = new_site(domain: "regular.com")
      _consolidated_site = new_site(domain: "consolidated.com", consolidated: true)

      assert Sites.get_by_domain("regular.com", include_consolidated?: true)
      assert Sites.get_by_domain("consolidated.com", include_consolidated?: true)
    end
  end

  describe "predicates for site.consolidated" do
    on_ee do
      test "regular?/1" do
        assert Sites.regular?(%Plausible.Site{consolidated: false})
        refute Sites.regular?(%Plausible.Site{consolidated: true})
      end

      test "consolidated?/1" do
        assert Sites.consolidated?(%Plausible.Site{consolidated: true})
        refute Sites.consolidated?(%Plausible.Site{consolidated: false})
      end
    else
      test "all sites are regular on CE" do
        assert Sites.regular?(%Plausible.Site{})
        refute Sites.consolidated?(%Plausible.Site{})
      end
    end
  end
end
