defmodule WhoNeedHelp.TrustSafetyTest do
  use WhoNeedHelp.DataCase, async: false

  import WhoNeedHelp.AccountsFixtures

  alias WhoNeedHelp.{Catalog, Help, Messaging, Release, Tracking, Trust}
  alias WhoNeedHelp.Help.Assignment
  alias WhoNeedHelp.Repo
  alias WhoNeedHelp.Tracking.Position
  alias WhoNeedHelp.Trust.{AbuseSignal, AuditEvent, RateLimiter}

  setup do
    category = Catalog.seed_defaults()
    requester = user_fixture(display_name: "Requester")
    helper = user_fixture(display_name: "Helper")

    attrs = %{
      "title" => "Medicine is ready at the pharmacy",
      "description" => "The legal medicine is already reserved and ready for pickup.",
      "location_label" => "Central district",
      "latitude" => "50.4501",
      "longitude" => "30.5234",
      "urgency" => "now",
      "location_visibility" => "approximate_public",
      "structured_data" => %{"pickup_status" => "reserved"},
      "expires_at" => DateTime.utc_now(:second) |> DateTime.add(3, :hour),
      "category_id" => category.id,
      "safety_confirmed" => true
    }

    %{
      category: category,
      requester: requester,
      helper: helper,
      requester_scope: user_scope_fixture(requester),
      helper_scope: user_scope_fixture(helper),
      attrs: attrs
    }
  end

  test "a block removes discovery and prevents matching and new chat", context do
    {:ok, request} = Help.create_request(context.requester_scope, context.attrs)
    assert Enum.any?(Help.list_open_requests(context.helper_scope), &(&1.id == request.id))

    assert {:ok, _block} = Trust.block(context.helper_scope, context.requester.id)
    refute Enum.any?(Help.list_open_requests(context.helper_scope), &(&1.id == request.id))
    assert {:error, :blocked} = Help.accept_request(context.helper_scope, request.id)

    assert {:ok, :unblocked} = Trust.unblock(context.helper_scope, context.requester.id)
    assert {:ok, assignment} = Help.accept_request(context.helper_scope, request.id)

    assert {:ok, _block} = Trust.block(context.requester_scope, context.helper.id)

    assert {:error, :blocked} =
             Messaging.send_message(context.helper_scope, assignment, %{"body" => "Hello"})

    assert [] = Messaging.list_messages(context.requester_scope, assignment)
  end

  test "invalid block identifiers are rejected without a query cast failure", context do
    assert {:error, :not_found} = Trust.block(context.requester_scope, "not-a-uuid")
    assert {:error, :not_found} = Trust.unblock(context.requester_scope, "not-a-uuid")
  end

  test "invalid report target identifiers are rejected without a query cast failure", context do
    for target <- ~w(request_id assignment_id message_id activity_id activity_message_id) do
      assert {:error, :not_found} =
               Trust.report(context.requester_scope, %{
                 target => "not-a-uuid",
                 "reason" => "unsafe",
                 "details" => "Malformed target identifiers must not reach Ecto queries."
               })
    end
  end

  test "forged moderation identifiers return not found without crashing", context do
    moderator =
      user_fixture(display_name: "Moderator")
      |> Ecto.Changeset.change(role: :moderator)
      |> Repo.update!()

    admin =
      user_fixture(display_name: "Administrator")
      |> Ecto.Changeset.change(role: :admin)
      |> Repo.update!()

    moderator_scope = user_scope_fixture(moderator)
    admin_scope = user_scope_fixture(admin)
    missing_id = Ecto.UUID.generate()

    for id <- ["not-a-uuid", missing_id] do
      assert {:error, :not_found} = Trust.moderate_report(moderator_scope, id, %{})
      assert {:error, :not_found} = Trust.report_evidence(moderator_scope, id)
      assert {:error, :not_found} = Trust.moderate_signal(moderator_scope, id, %{})
      assert {:error, :not_found} = Trust.hide_request(moderator_scope, id, "review")
      assert {:error, :not_found} = Trust.restore_request(moderator_scope, id)
      assert {:error, :not_found} = Trust.hide_activity(moderator_scope, id, "review")
      assert {:error, :not_found} = Trust.restore_activity(moderator_scope, id)
      assert {:error, :not_found} = Trust.moderate_user(moderator_scope, id, %{})
      assert {:error, :not_found} = Trust.moderate_role(admin_scope, id, %{})
      assert {:error, :not_found} = Catalog.approve_proposal(moderator_scope, id, %{})
      assert {:error, :not_found} = Catalog.reject_proposal(moderator_scope, id, "review")

      assert {:error, :not_found} =
               Catalog.merge_proposal(moderator_scope, id, context.category.id, "review")
    end

    assert {:error, :not_found} =
             Catalog.merge_proposal(
               moderator_scope,
               Ecto.UUID.generate(),
               "not-a-uuid",
               "review"
             )
  end

  test "tracking derives movement and proximity from browser accuracy envelopes", context do
    {:ok, request} = Help.create_request(context.requester_scope, context.attrs)
    {:ok, assignment} = Help.accept_request(context.helper_scope, request.id)
    :ok = Tracking.subscribe(assignment.id)
    {:ok, _} = Tracking.start_session(context.helper_scope, assignment)
    {:ok, _} = Tracking.start_session(context.requester_scope, assignment)

    {:ok, _} =
      Tracking.update_position(context.helper_scope, assignment, %{
        "latitude" => 50.4500,
        "longitude" => 30.5200,
        "accuracy_meters" => 5.0
      })

    assert_receive {:position_updated, helper_id, _position,
                    %{helper_movement_observed_at: nil, proximity_observed_at: nil}}

    assert helper_id == context.helper.id

    {:ok, _} =
      Tracking.update_position(context.helper_scope, assignment, %{
        "latitude" => 50.4510,
        "longitude" => 30.5200,
        "accuracy_meters" => 5.0
      })

    assert_receive {:position_updated, helper_id, _position,
                    %{helper_movement_observed_at: movement_at, proximity_observed_at: nil}}

    assert helper_id == context.helper.id
    assert %DateTime{} = movement_at

    {:ok, _} =
      Tracking.update_position(context.requester_scope, assignment, %{
        "latitude" => 50.4511,
        "longitude" => 30.5200,
        "accuracy_meters" => 20.0
      })

    assignment = Repo.get!(Assignment, assignment.id)
    assert assignment.helper_movement_observed_at
    assert assignment.proximity_observed_at
  end

  test "leaderboard primary counters use unique counterparts", context do
    first = complete_supported_help(context, context.attrs)
    second_attrs = Map.put(context.attrs, "title", "A second pharmacy pickup is ready")
    second = complete_supported_help(context, second_attrs)

    [entry] = Enum.filter(Trust.leaderboard(), &(&1.user.id == context.helper.id))
    assert entry.completed == 2
    assert entry.unique_people == 1
    assert entry.verified_people == 1
    assert entry.location_supported_people == 1
    assert is_nil(entry.user.email)
    assert is_nil(entry.user.hashed_password)

    assert Repo.exists?(
             from signal in AbuseSignal,
               where: signal.assignment_id == ^second.id and signal.kind == :repeated_pair
           )

    refute first.id == second.id
  end

  test "leaderboard cursor preserves the complete ranking without duplicates", context do
    complete_supported_help(context, context.attrs)

    helpers =
      for {name, suffix} <- [{"Alpha helper", "alpha"}, {"Beta helper", "beta"}] do
        helper = user_fixture(display_name: name)
        helper_scope = user_scope_fixture(helper)

        attrs =
          context.attrs
          |> Map.put("title", "Medicine pickup for #{suffix}")

        complete_supported_help(context, attrs, helper_scope)
        helper
      end

    first = Trust.paginate_leaderboard(limit: 2)
    second = Trust.paginate_leaderboard(limit: 2, after: first.next_cursor)
    ids = Enum.map(first.entries ++ second.entries, & &1.user.id)

    assert length(ids) == 3
    assert length(Enum.uniq(ids)) == 3
    assert context.helper.id in ids
    assert Enum.all?(helpers, &(&1.id in ids))
    assert first.next_cursor
    assert second.next_cursor == nil
  end

  test "reports expose only linked evidence to moderators and audit that access", context do
    moderator =
      user_fixture(display_name: "Moderator")
      |> Ecto.Changeset.change(role: :moderator)
      |> Repo.update!()

    moderator_scope = user_scope_fixture(moderator)
    {:ok, request} = Help.create_request(context.requester_scope, context.attrs)
    {:ok, assignment} = Help.accept_request(context.helper_scope, request.id)

    {:ok, _message} =
      Messaging.send_message(context.helper_scope, assignment, %{"body" => "Pickup update"})

    {:ok, report} =
      Trust.report(context.requester_scope, %{
        "reason" => "harassment",
        "details" => "This conversation needs a moderator review.",
        "assignment_id" => assignment.id,
        "reporter_id" => context.helper.id
      })

    assert report.reporter_id == context.requester.id
    assert [listed] = Trust.list_reports(moderator_scope, :open)
    assert listed.id == report.id

    assert {:ok, %{messages: [%{body: "Pickup update"}]}} =
             Trust.report_evidence(moderator_scope, report.id)

    assert Repo.exists?(
             from event in AuditEvent,
               where:
                 event.action == "report.evidence_viewed" and event.target_id == ^report.id and
                   event.actor_id == ^moderator.id
           )
  end

  test "medicine category structured fields are enforced by the server", context do
    invalid = Map.delete(context.attrs, "structured_data")
    assert {:error, changeset} = Help.create_request(context.requester_scope, invalid)
    assert "pickup_status is required" in errors_on(changeset).structured_data

    invalid =
      Map.put(context.attrs, "structured_data", %{"pickup_status" => "not-an-option"})

    assert {:error, changeset} = Help.create_request(context.requester_scope, invalid)
    assert "pickup_status has an invalid value" in errors_on(changeset).structured_data

    nested =
      Map.put(context.attrs, "structured_data", %{
        "pickup_status" => %{"unexpected" => "reserved"}
      })

    assert {:error, changeset} = Help.create_request(context.requester_scope, nested)
    assert "pickup_status has an invalid value" in errors_on(changeset).structured_data
  end

  test "help creation rejects missing safety consent and non-help categories", context do
    without_consent = Map.delete(context.attrs, "safety_confirmed")

    assert {:error, changeset} =
             Help.create_request(context.requester_scope, without_consent)

    assert "confirm the safety guidance before publishing" in errors_on(changeset).safety_confirmed

    activity_category =
      Catalog.list_categories(:activity)
      |> List.first()

    wrong_category = Map.put(context.attrs, "category_id", activity_category.id)

    assert {:error, changeset} =
             Help.create_request(context.requester_scope, wrong_category)

    assert "select an active help category" in errors_on(changeset).category_id
  end

  test "configured rate limits are atomic database counters", context do
    old = Application.get_env(:who_need_help, :rate_limit_policies)

    Application.put_env(:who_need_help, :rate_limit_policies, %{
      "test_action" => %{"limit" => 1, "window_seconds" => 60}
    })

    on_exit(fn -> Application.put_env(:who_need_help, :rate_limit_policies, old) end)

    assert {:ok, %{count: 1}} = RateLimiter.check(:test_action, context.helper.id)
    assert {:error, :rate_limited} = RateLimiter.check(:test_action, context.helper.id)
    assert {:ok, %{count: 1}} = RateLimiter.check(:test_action, context.requester.id)

    assert {:error, :rate_limited} =
             Trust.authorize_action(context.helper_scope, :test_action)

    assert Repo.exists?(
             from signal in AbuseSignal,
               where: signal.subject_id == ^context.helper.id and signal.kind == :velocity
           )
  end

  test "malformed runtime rate-limit configuration never crashes a request", context do
    old = Application.get_env(:who_need_help, :rate_limit_policies)
    Application.put_env(:who_need_help, :rate_limit_policies, ["not", "a", "map"])
    on_exit(fn -> Application.put_env(:who_need_help, :rate_limit_policies, old) end)

    assert {:ok, :not_configured} =
             RateLimiter.check(:test_action, context.requester.id)
  end

  test "restricted accounts cannot perform trust-sensitive actions", context do
    context.helper
    |> WhoNeedHelp.Accounts.User.moderation_changeset(%{moderation_status: :restricted})
    |> Repo.update!()

    assert {:error, :account_not_eligible} =
             Help.create_request(context.helper_scope, context.attrs)
  end

  test "hidden request coordinates remain hidden from an accepted helper", context do
    hidden_attrs = Map.put(context.attrs, "location_visibility", "hidden")
    {:ok, request} = Help.create_request(context.requester_scope, hidden_attrs)
    {:ok, _assignment} = Help.accept_request(context.helper_scope, request.id)
    request = Help.get_request!(request.id)
    outsider_scope = user_fixture(display_name: "Outsider") |> user_scope_fixture()

    assert is_nil(WhoNeedHelp.Help.HelpRequest.public_coordinates(request))
    assert is_nil(Help.request_coordinates(outsider_scope, request))
    assert is_nil(Help.request_coordinates(context.helper_scope, request))

    assert %{latitude: 50.4501, longitude: 30.5234, exact: true} =
             Help.request_coordinates(context.requester_scope, request)
  end

  test "blocking an accepted helper revokes request, exact location, and tracking", context do
    attrs = Map.put(context.attrs, "location_visibility", "exact_for_active_match")
    {:ok, request} = Help.create_request(context.requester_scope, attrs)
    {:ok, assignment} = Help.accept_request(context.helper_scope, request.id)
    request = Help.get_request!(request.id)

    {:ok, _session} = Tracking.start_session(context.helper_scope, assignment)

    {:ok, position} =
      Tracking.update_position(context.helper_scope, assignment, %{
        "latitude" => 50.45,
        "longitude" => 30.52,
        "accuracy_meters" => 10.0
      })

    assert %{exact: true} = Help.request_coordinates(context.helper_scope, request)
    assert {:ok, _block} = Trust.block(context.requester_scope, context.helper.id)

    assert {:error, :not_found} = Help.get_request(context.helper_scope, request.id)
    assert %{exact: false} = Help.request_coordinates(context.helper_scope, request)
    assert Tracking.list_current_positions(context.helper_scope, assignment) == %{}
    assert {:error, :blocked} = Tracking.start_session(context.helper_scope, assignment)

    assert {:error, :blocked} =
             Tracking.update_position(context.helper_scope, assignment, %{
               "latitude" => 50.46,
               "longitude" => 30.53,
               "accuracy_meters" => 10.0
             })

    refute Repo.get(Position, position.id)
  end

  test "cancelling a matched request ends tracking and deletes raw coordinates", context do
    {:ok, request} = Help.create_request(context.requester_scope, context.attrs)
    {:ok, assignment} = Help.accept_request(context.helper_scope, request.id)
    {:ok, _session} = Tracking.start_session(context.helper_scope, assignment)

    {:ok, position} =
      Tracking.update_position(context.helper_scope, assignment, %{
        "latitude" => 50.45,
        "longitude" => 30.52,
        "accuracy_meters" => 10.0
      })

    assert Repo.get(Position, position.id)
    assert {:ok, _request} = Help.cancel_request(context.requester_scope, request.id)
    refute Repo.get(Position, position.id)
    assert Repo.get!(Assignment, assignment.id).status == :cancelled
  end

  test "only an administrator can change roles and the last admin is protected", context do
    moderator =
      user_fixture(display_name: "Moderator")
      |> Ecto.Changeset.change(role: :moderator)
      |> Repo.update!()

    admin =
      user_fixture(display_name: "Administrator")
      |> Ecto.Changeset.change(role: :admin)
      |> Repo.update!()

    assert {:error, :forbidden} =
             Trust.moderate_role(user_scope_fixture(moderator), context.helper.id, %{
               "role" => "moderator"
             })

    assert {:ok, promoted} =
             Trust.moderate_role(user_scope_fixture(admin), context.helper.id, %{
               "role" => "moderator"
             })

    assert promoted.role == :moderator

    assert {:error, :last_admin} =
             Trust.moderate_role(user_scope_fixture(admin), admin.id, %{"role" => "user"})

    assert Repo.exists?(
             from event in AuditEvent,
               where:
                 event.actor_id == ^admin.id and event.target_id == ^context.helper.id and
                   event.action == "user.role_changed"
           )
  end

  test "restricted moderators lose authorization and the last active admin cannot be suspended",
       context do
    admin =
      user_fixture(display_name: "Administrator")
      |> Ecto.Changeset.change(role: :admin)
      |> Repo.update!()

    moderator =
      user_fixture(display_name: "Moderator")
      |> Ecto.Changeset.change(role: :moderator)
      |> Repo.update!()

    active_moderator =
      user_fixture(display_name: "Active moderator")
      |> Ecto.Changeset.change(role: :moderator)
      |> Repo.update!()

    assert WhoNeedHelp.Accounts.moderator_authorized?(moderator)

    restricted =
      moderator
      |> WhoNeedHelp.Accounts.User.moderation_changeset(%{
        "moderation_status" => "restricted"
      })
      |> Repo.update!()

    refute WhoNeedHelp.Accounts.moderator_authorized?(restricted)

    session_token = WhoNeedHelp.Accounts.generate_user_session_token(context.helper)
    socket_id = WhoNeedHelpWeb.UserAuth.live_socket_id(session_token)
    :ok = WhoNeedHelpWeb.Endpoint.subscribe(socket_id)

    {:ok, request} = Help.create_request(context.requester_scope, context.attrs)
    {:ok, assignment} = Help.accept_request(context.helper_scope, request.id)
    {:ok, tracking_session} = Tracking.start_session(context.helper_scope, assignment)

    {:ok, _position} =
      Tracking.update_position(context.helper_scope, assignment, %{
        "latitude" => 50.4501,
        "longitude" => 30.5234,
        "accuracy_meters" => 5.0
      })

    :ok = Tracking.subscribe(assignment.id)

    assert {:ok, suspended} =
             Trust.moderate_user(user_scope_fixture(active_moderator), context.helper.id, %{
               "moderation_status" => "suspended"
             })

    assert suspended.moderation_status == :suspended
    assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^socket_id}
    assert_receive {:tracking_stopped, helper_id}
    assert helper_id == context.helper.id
    assert WhoNeedHelp.Accounts.get_user_by_session_token(session_token) == nil
    refute Repo.get_by(Position, tracking_session_id: tracking_session.id)

    refute Repo.get_by(WhoNeedHelp.Tracking.TrackingSession,
             id: tracking_session.id,
             active: true
           )

    assert {:error, :last_admin} =
             Trust.moderate_user(user_scope_fixture(active_moderator), admin.id, %{
               "moderation_status" => "suspended"
             })

    assert Repo.get!(WhoNeedHelp.Accounts.User, admin.id).moderation_status == :active
  end

  test "the first administrator bootstrap is one-time and audited", context do
    assert {:ok, admin} = Release.bootstrap_admin(context.helper.email)
    assert admin.id == context.helper.id
    assert admin.role == :admin

    assert {:error, :admin_already_exists} =
             Release.bootstrap_admin(context.requester.email)

    assert Repo.exists?(
             from event in AuditEvent,
               where:
                 is_nil(event.actor_id) and event.target_id == ^context.helper.id and
                   event.action == "user.admin_bootstrapped"
           )
  end

  defp complete_supported_help(context, attrs, helper_scope \\ nil) do
    helper_scope = helper_scope || context.helper_scope
    {:ok, request} = Help.create_request(context.requester_scope, attrs)
    {:ok, assignment} = Help.accept_request(helper_scope, request.id)

    assignment =
      assignment
      |> Assignment.changeset(%{
        proximity_observed_at: DateTime.utc_now(:second),
        helper_movement_observed_at: DateTime.utc_now(:second)
      })
      |> Repo.update!()

    {:ok, _} = Help.confirm_completion(context.requester_scope, assignment.id)
    {:ok, _} = Help.confirm_completion(helper_scope, assignment.id)

    {:ok, assignment} =
      Help.verify_handover(helper_scope, assignment.id, Help.handover_code(request.id))

    assignment
  end
end
