require "test_helper"

class SessionsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:family_admin)
  end

  teardown do
    # Clear OmniAuth mock auth after each test
    OmniAuth.config.mock_auth[:openid_connect] = nil
  end

  def setup_omniauth_mock(provider:, uid:, email:, name:, first_name: nil, last_name: nil)
    OmniAuth.config.mock_auth[:openid_connect] = OmniAuth::AuthHash.new({
      provider: provider,
      uid: uid,
      info: {
        email: email,
        name: name,
        first_name: first_name,
        last_name: last_name
      }.compact
    })
  end

  test "login page" do
    get new_session_url
    assert_response :success
  end

  test "can sign in" do
    sign_in @user
    assert_redirected_to root_url
    assert Session.exists?(user_id: @user.id)

    get root_url
    assert_response :success
  end

  test "fails to sign in with bad password" do
    post sessions_url, params: { email: @user.email, password: "bad" }
    assert_response :unprocessable_entity
    assert_equal "Invalid email or password.", flash[:alert]
  end

  test "redirects when local login is disabled" do
    AuthConfig.stubs(:local_login_enabled?).returns(false)
    AuthConfig.stubs(:local_admin_override_enabled?).returns(false)

    post sessions_url, params: { email: @user.email, password: user_password_test }

    assert_redirected_to new_session_path
    assert_equal "Local password login is disabled. Please use single sign-on.", flash[:alert]
  end

  test "allows super admin local login when override enabled" do
    super_admin = users(:sure_support_staff)

    AuthConfig.stubs(:local_login_enabled?).returns(false)
    AuthConfig.stubs(:local_admin_override_enabled?).returns(true)

    post sessions_url, params: { email: super_admin.email, password: user_password_test }

    assert_redirected_to root_path
    assert Session.exists?(user_id: super_admin.id)
  end

  test "shows invalid credentials for super admin when override enabled but password is wrong" do
    super_admin = users(:sure_support_staff)

    AuthConfig.stubs(:local_login_enabled?).returns(false)
    AuthConfig.stubs(:local_admin_override_enabled?).returns(true)

    post sessions_url, params: { email: super_admin.email, password: "bad" }

    assert_response :unprocessable_entity
    assert_equal "Invalid email or password.", flash[:alert]
  end

  test "blocks non-super-admin local login when override enabled" do
    AuthConfig.stubs(:local_login_enabled?).returns(false)
    AuthConfig.stubs(:local_admin_override_enabled?).returns(true)

    post sessions_url, params: { email: @user.email, password: user_password_test }

    assert_redirected_to new_session_path
    assert_equal "Local password login is disabled. Please use single sign-on.", flash[:alert]
  end

  test "renders multiple SSO provider buttons" do
    AuthConfig.stubs(:local_login_form_visible?).returns(true)
    AuthConfig.stubs(:password_features_enabled?).returns(true)
    AuthConfig.stubs(:sso_providers).returns([
      { id: "oidc", strategy: "openid_connect", name: "openid_connect", label: "Sign in with Keycloak", icon: "key" },
      { id: "google", strategy: "google_oauth2", name: "google_oauth2", label: "Sign in with Google", icon: "google" }
    ])

    get new_session_path
    assert_response :success

    # Generic OIDC button
    assert_match %r{/auth/openid_connect}, @response.body
    assert_match /Sign in with Keycloak/, @response.body

    # Google-branded button
    assert_match %r{/auth/google_oauth2}, @response.body
    assert_match /gsi-material-button/, @response.body
    assert_match /Sign in with Google/, @response.body
  end

  test "can sign out" do
    sign_in @user
    session_record = @user.sessions.last

    delete session_url(session_record)
    assert_redirected_to new_session_path
    assert_equal "You have signed out successfully.", flash[:notice]

    # Verify session is destroyed
    assert_nil Session.find_by(id: session_record.id)
  end

  test "redirects to MFA verification when MFA enabled" do
    @user.setup_mfa!
    @user.enable_mfa!
    @user.sessions.destroy_all # Clean up any existing sessions

    post sessions_path, params: { email: @user.email, password: user_password_test }

    assert_redirected_to verify_mfa_path
    assert_equal @user.id, session[:mfa_user_id]
    assert_not Session.exists?(user_id: @user.id)
  end

  # OIDC Authentication Tests
  test "authenticates with existing OIDC identity" do
    oidc_identity = oidc_identities(:bob_google)

    # Set up OmniAuth mock
    setup_omniauth_mock(
      provider: oidc_identity.provider,
      uid: oidc_identity.uid,
      email: @user.email,
      name: "Bob Dylan",
      first_name: "Bob",
      last_name: "Dylan"
    )

    get "/auth/openid_connect/callback"

    assert_redirected_to root_path
    assert Session.exists?(user_id: @user.id)
  end

  test "redirects to MFA when user has MFA and uses OIDC" do
    @user.setup_mfa!
    @user.enable_mfa!
    @user.sessions.destroy_all
    oidc_identity = oidc_identities(:bob_google)

    # Set up OmniAuth mock
    setup_omniauth_mock(
      provider: oidc_identity.provider,
      uid: oidc_identity.uid,
      email: @user.email,
      name: "Bob Dylan"
    )

    get "/auth/openid_connect/callback"

    assert_redirected_to verify_mfa_path
    assert_equal @user.id, session[:mfa_user_id]
    assert_not Session.exists?(user_id: @user.id)
  end

  test "redirects to account linking when no OIDC identity exists" do
    # Use an existing user's email who doesn't have OIDC linked yet
    user_without_oidc = users(:new_email)

    # Set up OmniAuth mock
    setup_omniauth_mock(
      provider: "openid_connect",
      uid: "new-uid-99999",
      email: user_without_oidc.email,
      name: "New User"
    )

    get "/auth/openid_connect/callback"

    assert_redirected_to link_oidc_account_path

    # Follow redirect to verify session data is accessible
    follow_redirect!
    assert_response :success

    # Verify the session has the pending auth data by checking page content
    assert_select "p", text: /To link your openid_connect account/
  end

  test "handles missing auth data gracefully" do
    # Set up mock with invalid/incomplete auth to simulate failure
    OmniAuth.config.mock_auth[:openid_connect] = OmniAuth::AuthHash.new({
      provider: nil,
      uid: nil
    })

    get "/auth/openid_connect/callback"

    assert_redirected_to new_session_path
    assert_equal "Could not authenticate via OpenID Connect.", flash[:alert]
  end

  test "prevents account takeover via email matching" do
    # Clean up any existing sessions
    @user.sessions.destroy_all

    # This test verifies that we can't authenticate just by matching email
    # The user must have an existing OIDC identity with matching provider + uid
    # Set up OmniAuth mock
    setup_omniauth_mock(
      provider: "openid_connect",
      uid: "attacker-uid-12345", # Different UID than user's OIDC identity
      email: @user.email, # Same email as existing user
      name: "Attacker"
    )

    get "/auth/openid_connect/callback"

    # Should NOT create a session, should redirect to account linking
    assert_redirected_to link_oidc_account_path
    assert_not Session.exists?(user_id: @user.id), "Session should not be created for unlinked OIDC identity"

    # Follow redirect to verify we're on the link page (not logged in)
    follow_redirect!
    assert_response :success
  end
end
