defmodule Towerops.MobileSessionsTest do
  use Towerops.DataCase

  import Towerops.AccountsFixtures

  alias Towerops.MobileSessions
  alias Towerops.MobileSessions.MobileSession
  alias Towerops.MobileSessions.QRLoginToken

  defp valid_session_attrs(user) do
    %{
      user_id: user.id,
      device_name: "iPhone 15 Pro",
      device_os: "iOS 17.2",
      app_version: "1.0.0"
    }
  end

  defp create_session(user, overrides \\ %{}) do
    attrs = Map.merge(valid_session_attrs(user), overrides)
    {:ok, session} = MobileSessions.create_mobile_session(attrs)
    session
  end

  describe "create_mobile_session/1" do
    test "creates session with valid attrs" do
      user = user_fixture()

      assert {:ok, %MobileSession{} = session} =
               MobileSessions.create_mobile_session(valid_session_attrs(user))

      assert session.user_id == user.id
      assert session.device_name == "iPhone 15 Pro"
      assert session.device_os == "iOS 17.2"
      assert session.app_version == "1.0.0"
      assert session.token
      assert session.expires_at
      assert session.last_used_at
      assert session.alerts_enabled == true
    end

    test "returns error changeset with invalid attrs (missing user_id)" do
      assert {:error, %Ecto.Changeset{}} =
               MobileSessions.create_mobile_session(%{device_name: "Test"})
    end
  end

  describe "get_session/1" do
    test "returns session by id" do
      user = user_fixture()
      session = create_session(user)

      assert found = MobileSessions.get_session(session.id)
      assert found.id == session.id
    end

    test "returns nil for non-existent id" do
      assert MobileSessions.get_session(Ecto.UUID.generate()) == nil
    end
  end

  describe "get_session_by_token/1" do
    test "returns session for valid token" do
      user = user_fixture()
      session = create_session(user)

      assert found = MobileSessions.get_session_by_token(session.token)
      assert found.id == session.id
    end

    test "returns nil for expired session" do
      user = user_fixture()
      expired = create_session(user, %{expires_at: DateTime.add(DateTime.utc_now(), -1, :day)})

      assert MobileSessions.get_session_by_token(expired.token) == nil
    end

    test "returns nil for non-existent token" do
      assert MobileSessions.get_session_by_token("nonexistent-token") == nil
    end

    test "returns nil for non-binary input" do
      assert MobileSessions.get_session_by_token(nil) == nil
      assert MobileSessions.get_session_by_token(123) == nil
    end
  end

  describe "touch_session/1" do
    test "updates last_used_at" do
      user = user_fixture()
      session = create_session(user)
      original_last_used_at = session.last_used_at

      # Small sleep to ensure time difference
      Process.sleep(1000)

      assert {:ok, touched} = MobileSessions.touch_session(session)
      assert DateTime.compare(touched.last_used_at, original_last_used_at) in [:gt, :eq]
    end
  end

  describe "list_user_sessions/1" do
    test "returns active sessions for user ordered by last_used_at desc" do
      user = user_fixture()
      session1 = create_session(user, %{device_name: "Device A"})

      Process.sleep(1000)
      session2 = create_session(user, %{device_name: "Device B"})

      sessions = MobileSessions.list_user_sessions(user.id)
      assert length(sessions) == 2
      assert hd(sessions).id == session2.id
      assert List.last(sessions).id == session1.id
    end

    test "excludes expired sessions" do
      user = user_fixture()
      _active = create_session(user, %{device_name: "Active"})

      _expired =
        create_session(user, %{
          device_name: "Expired",
          expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
        })

      sessions = MobileSessions.list_user_sessions(user.id)
      assert length(sessions) == 1
      assert hd(sessions).device_name == "Active"
    end

    test "returns empty list for no sessions" do
      user = user_fixture()
      assert MobileSessions.list_user_sessions(user.id) == []
    end
  end

  describe "revoke_session/1" do
    test "deletes session by id" do
      user = user_fixture()
      session = create_session(user)

      assert {:ok, %MobileSession{}} = MobileSessions.revoke_session(session.id)
      assert MobileSessions.get_session(session.id) == nil
    end

    test "returns error for non-existent id" do
      assert {:error, :not_found} = MobileSessions.revoke_session(Ecto.UUID.generate())
    end
  end

  describe "update_alert_preferences/2" do
    test "updates alerts_enabled" do
      user = user_fixture()
      session = create_session(user)

      assert {:ok, updated} =
               MobileSessions.update_alert_preferences(session.id, %{alerts_enabled: false})

      assert updated.alerts_enabled == false
    end

    test "updates push_token" do
      user = user_fixture()
      session = create_session(user)

      assert {:ok, updated} =
               MobileSessions.update_alert_preferences(session.id, %{
                 push_token: "device-push-token-abc",
                 push_platform: "apns"
               })

      assert updated.push_token == "device-push-token-abc"
      assert updated.push_platform == "apns"
    end

    test "returns error for non-existent session" do
      assert {:error, :not_found} =
               MobileSessions.update_alert_preferences(Ecto.UUID.generate(), %{
                 alerts_enabled: false
               })
    end
  end

  describe "list_alert_enabled_devices/1" do
    test "returns sessions with alerts enabled and push_token set" do
      user = user_fixture()
      session = create_session(user)

      {:ok, _updated} =
        MobileSessions.update_alert_preferences(session.id, %{
          alerts_enabled: true,
          push_token: "token-abc",
          push_platform: "apns"
        })

      devices = MobileSessions.list_alert_enabled_devices(user.id)
      assert length(devices) == 1
      assert hd(devices).id == session.id
    end

    test "excludes sessions with alerts disabled" do
      user = user_fixture()
      session = create_session(user)

      {:ok, _updated} =
        MobileSessions.update_alert_preferences(session.id, %{
          alerts_enabled: false,
          push_token: "token-abc"
        })

      assert MobileSessions.list_alert_enabled_devices(user.id) == []
    end

    test "excludes sessions without push_token" do
      user = user_fixture()
      _session = create_session(user, %{alerts_enabled: true})

      assert MobileSessions.list_alert_enabled_devices(user.id) == []
    end

    test "excludes expired sessions" do
      user = user_fixture()

      session =
        create_session(user, %{
          expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
        })

      {:ok, _updated} =
        MobileSessions.update_alert_preferences(session.id, %{
          alerts_enabled: true,
          push_token: "token-abc"
        })

      assert MobileSessions.list_alert_enabled_devices(user.id) == []
    end
  end

  describe "revoke_all_user_sessions/1" do
    test "deletes all sessions for user, returns count" do
      user = user_fixture()
      _session1 = create_session(user, %{device_name: "Device A"})
      _session2 = create_session(user, %{device_name: "Device B"})

      assert {:ok, 2} = MobileSessions.revoke_all_user_sessions(user.id)
      assert MobileSessions.list_user_sessions(user.id) == []
    end

    test "returns 0 when no sessions" do
      user = user_fixture()
      assert {:ok, 0} = MobileSessions.revoke_all_user_sessions(user.id)
    end
  end

  describe "delete_expired_sessions/0" do
    test "deletes expired sessions, returns count" do
      user = user_fixture()

      _expired1 =
        create_session(user, %{
          device_name: "Expired 1",
          expires_at: DateTime.add(DateTime.utc_now(), -2, :day)
        })

      _expired2 =
        create_session(user, %{
          device_name: "Expired 2",
          expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
        })

      assert MobileSessions.delete_expired_sessions() == 2
    end

    test "does not delete active sessions" do
      user = user_fixture()
      active = create_session(user, %{device_name: "Active"})

      _expired =
        create_session(user, %{
          device_name: "Expired",
          expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
        })

      assert MobileSessions.delete_expired_sessions() == 1
      assert MobileSessions.get_session(active.id)
    end
  end

  describe "create_qr_login_token/1" do
    test "creates token for user" do
      user = user_fixture()

      assert {:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
      assert qr_token.user_id == user.id
      assert qr_token.token
      assert qr_token.expires_at
      assert qr_token.completed_at == nil
    end
  end

  describe "get_qr_login_token/1" do
    test "returns token for valid token string" do
      user = user_fixture()
      {:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)

      assert found = MobileSessions.get_qr_login_token(qr_token.token)
      assert found.id == qr_token.id
    end

    test "returns nil for expired token" do
      user = user_fixture()

      {:ok, qr_token} =
        %QRLoginToken{}
        |> QRLoginToken.create_changeset(%{
          user_id: user.id,
          expires_at: DateTime.add(DateTime.utc_now(), -1, :minute)
        })
        |> Repo.insert()

      assert MobileSessions.get_qr_login_token(qr_token.token) == nil
    end

    test "returns nil for completed token" do
      user = user_fixture()
      {:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)

      # Complete the token via the normal flow so it has a valid mobile_session_id
      device_attrs = %{device_name: "Test Device", device_os: "iOS", app_version: "1.0"}
      {:ok, _session} = MobileSessions.complete_qr_login(qr_token.token, device_attrs)

      assert MobileSessions.get_qr_login_token(qr_token.token) == nil
    end

    test "returns nil for non-binary input" do
      assert MobileSessions.get_qr_login_token(nil) == nil
      assert MobileSessions.get_qr_login_token(123) == nil
    end
  end

  describe "complete_qr_login/2" do
    test "creates mobile session and marks token completed" do
      user = user_fixture()
      {:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)

      device_attrs = %{
        device_name: "iPhone 15",
        device_os: "iOS 17.2",
        app_version: "1.0.0"
      }

      assert {:ok, session} = MobileSessions.complete_qr_login(qr_token.token, device_attrs)
      assert session.user_id == user.id
      assert session.device_name == "iPhone 15"

      # Verify QR token is marked completed
      assert MobileSessions.get_qr_login_token(qr_token.token) == nil
    end

    test "returns error for invalid token" do
      assert {:error, :invalid_token} =
               MobileSessions.complete_qr_login("invalid-token", %{
                 device_name: "Test",
                 device_os: "iOS"
               })
    end
  end

  describe "check_qr_login_completed/1" do
    test "returns mobile session for completed token" do
      user = user_fixture()
      {:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)

      device_attrs = %{
        device_name: "iPhone 15",
        device_os: "iOS 17.2",
        app_version: "1.0.0"
      }

      {:ok, session} = MobileSessions.complete_qr_login(qr_token.token, device_attrs)

      found = MobileSessions.check_qr_login_completed(qr_token.token)
      assert found.id == session.id
    end

    test "returns nil for incomplete token" do
      user = user_fixture()
      {:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)

      assert MobileSessions.check_qr_login_completed(qr_token.token) == nil
    end

    test "returns nil for non-binary input" do
      assert MobileSessions.check_qr_login_completed(nil) == nil
      assert MobileSessions.check_qr_login_completed(123) == nil
    end
  end

  describe "delete_expired_qr_tokens/0" do
    test "deletes expired tokens, returns count" do
      user = user_fixture()

      # Create an expired QR token
      {:ok, _expired} =
        %QRLoginToken{}
        |> QRLoginToken.create_changeset(%{
          user_id: user.id,
          expires_at: DateTime.add(DateTime.utc_now(), -10, :minute)
        })
        |> Repo.insert()

      assert MobileSessions.delete_expired_qr_tokens() >= 1
    end
  end
end
