require "minitest/autorun"
require "openid/consumer/checkid_request"
require "openid/message"
require "testutil"
require "util"

module OpenID
  class Consumer
    class CheckIDRequest
      class DummyEndpoint
        attr_accessor :preferred_namespace, :local_id, :server_url,
          :is_op_identifier, :claimed_id

        def initialize
          @preferred_namespace = nil
          @local_id = nil
          @server_url = nil
          @is_op_identifier = false
        end

        def get_local_id
          @local_id
        end

        def compatibility_mode
          @preferred_namespace == OPENID1_NS
        end
      end

      module CheckIDTestMixin
        include TestUtil

        def setup
          @endpoint = DummyEndpoint.new
          @endpoint.local_id = 'http://server.unittest/joe'
          @endpoint.claimed_id = 'http://joe.vanity.example/'
          @endpoint.server_url = 'http://server.unittest/'
          @endpoint.preferred_namespace = preferred_namespace
          @realm = 'http://example/'
          @return_to = 'http://example/return/'
          @assoc = GoodAssoc.new
          @checkid_req = CheckIDRequest.new(@assoc, @endpoint)
        end

        def assert_has_identifiers(msg, local_id, claimed_id)
          assert_openid_value_equal(msg, 'identity', local_id)
          assert_openid_value_equal(msg, 'claimed_id', claimed_id)
        end

        def assert_openid_key_exists(msg, key)
          assert(msg.get_arg(OPENID_NS, key),
                 "#{key} not present in #{msg.get_args(OPENID_NS).inspect}")
        end

        def assert_openid_key_absent(msg, key)
          assert(msg.get_arg(OPENID_NS, key).nil?)
        end

        def assert_openid_value_equal(msg, key, expected)
          actual = msg.get_arg(OPENID_NS, key, NO_DEFAULT)
          error_text = ("Expected #{expected.inspect} for openid.#{key} "\
                        "but got #{actual.inspect}: #{msg.inspect}")
          assert_equal(expected, actual, error_text)
        end

        def assert_anonymous(msg)
          ['claimed_id', 'identity'].each do |key|
            assert_openid_key_absent(msg, key)
          end
        end

        def assert_has_required_fields(msg)
          internal_message = @checkid_req.instance_variable_get(:@message)
          assert_equal(preferred_namespace,
                       internal_message.get_openid_namespace)

          assert_equal(preferred_namespace, msg.get_openid_namespace)
          assert_openid_value_equal(msg, 'mode', expected_mode)

          # Implement these in subclasses because they depend on
          # protocol differences!
          assert_has_realm(msg)
          assert_identifiers_present(msg)
        end

        # TESTS

        def test_check_no_assoc_handle
          @checkid_req.instance_variable_set('@assoc', nil)
          msg = assert_log_matches("Generated checkid") {
            @checkid_req.get_message(@realm, @return_to, immediate)
          }
          assert_openid_key_absent(msg, 'assoc_handle')
        end

        def test_add_extension_arg
          @checkid_req.add_extension_arg('bag:', 'color', 'brown')
          @checkid_req.add_extension_arg('bag:', 'material', 'paper')
          assert(@checkid_req.message.namespaces.member?('bag:'))
          assert_equal(@checkid_req.message.get_args('bag:'),
                       {'color' => 'brown', 'material' => 'paper'})

          msg = assert_log_matches("Generated checkid") {
            @checkid_req.get_message(@realm, @return_to, immediate)
          }

          # XXX: this depends on the way that Message assigns
          # namespaces. Really it doesn't care that it has alias "0",
          # but that is tested anyway
          post_args = msg.to_post_args()
          assert_equal('brown', post_args['openid.ext0.color'])
          assert_equal('paper', post_args['openid.ext0.material'])
        end

        def test_standard
          msg = assert_log_matches('Generated checkid') {
            @checkid_req.get_message(@realm, @return_to, immediate)
          }
          assert_has_identifiers(msg, @endpoint.local_id, @endpoint.claimed_id)
        end

        def test_send_redirect?
          silence_logging {
            url = @checkid_req.redirect_url(@realm, @return_to, immediate)
            assert(url.length < OPENID1_URL_LIMIT)
            assert(@checkid_req.send_redirect?(@realm, @return_to, immediate))

            @return_to << '/foo' * 1000
            url = @checkid_req.redirect_url(@realm, @return_to, immediate)
            assert(url.length > OPENID1_URL_LIMIT)
            actual = @checkid_req.send_redirect?(@realm, @return_to, immediate)
            expected = preferred_namespace != OPENID2_NS
            assert_equal(expected, actual)
          }
        end
      end

      class TestCheckIDRequestOpenID2 < Minitest::Test
        include CheckIDTestMixin

        def immediate
          false
        end

        def expected_mode
          'checkid_setup'
        end

        def preferred_namespace
          OPENID2_NS
        end

        # check presence of proper realm key and absence of the wrong
        # one.
        def assert_has_realm(msg)
          assert_openid_value_equal(msg, 'realm', @realm)
          assert_openid_key_absent(msg, 'trust_root')
        end

        def assert_identifiers_present(msg)
          identity_present = msg.has_key?(OPENID_NS, 'identity')
          claimed_present = msg.has_key?(OPENID_NS, 'claimed_id')

          assert_equal(claimed_present, identity_present)
        end

        # OpenID Checkid_Requests should be able to set 'anonymous' to true.
        def test_set_anonymous_works_for_openid2
          assert(@checkid_req.message.is_openid2)
          @checkid_req.anonymous = true
          @checkid_req.anonymous = false
        end

        def test_user_anonymous_ignores_identfier
          @checkid_req.anonymous = true
          msg = assert_log_matches('Generated checkid') {
            @checkid_req.get_message(@realm, @return_to, immediate)
          }
          assert_has_required_fields(msg)
          assert_anonymous(msg)
        end

        def test_op_anonymous_ignores_identifier
          @endpoint.is_op_identifier = true
          @checkid_req.anonymous = true
          msg = assert_log_matches('Generated checkid') {
            @checkid_req.get_message(@realm, @return_to, immediate)
          }
          assert_has_required_fields(msg)
          assert_anonymous(msg)
        end

        def test_op_identifier_sends_identifier_select
          @endpoint.is_op_identifier = true
          msg = assert_log_matches('Generated checkid') {
            @checkid_req.get_message(@realm, @return_to, immediate)
          }
          assert_has_required_fields(msg)
          assert_has_identifiers(msg, IDENTIFIER_SELECT, IDENTIFIER_SELECT)
        end

        def test_no_assoc_handle
          msg = assert_log_matches("Generated checkid") {
            @checkid_req.get_message(@realm, @return_to, immediate)
          }
          assert_openid_key_absent(msg, 'assoc_handle')
        end
      end

      class TestCheckIDRequestOpenID1 < Minitest::Test
        include CheckIDTestMixin

        def immediate
          false
        end

        def preferred_namespace
          OPENID1_NS
        end

        def expected_mode
          'checkid_setup'
        end

        # Make sure claimed_is is *absent* in request.
        def assert_has_identifiers(msg, op_specific_id, claimed_id)
          assert_openid_value_equal(msg, 'identity', op_specific_id)
          assert_openid_key_absent(msg, 'claimed_id')
        end

        def assert_identifiers_present(msg)
          assert_openid_key_absent(msg, 'claimed_id')
          assert(msg.has_key?(OPENID_NS, 'identity'))
        end

        # check presence of proper realm key and absence of the wrong
        # one.
        def assert_has_realm(msg)
          assert_openid_value_equal(msg, 'trust_root', @realm)
          assert_openid_key_absent(msg, 'realm')
        end

        # TESTS

        # OpenID 1 requests MUST NOT be able to set anonymous to true
        def test_set_anonymous_fails_for_openid1
          assert(@checkid_req.message.is_openid1)
          assert_raises(ArgumentError) {
            @checkid_req.anonymous = true
          }
          @checkid_req.anonymous = false
        end

        # Identfier select SHOULD NOT be sent, but this pathway is in
        # here in case some special discovery stuff is done to trigger
        # it with OpenID 1. If it is triggered, it will send
        # identifier_select just like OpenID 2.
        def test_identifier_select
          @endpoint.is_op_identifier = true
          msg = assert_log_matches('Generated checkid') {
            @checkid_req.get_message(@realm, @return_to, immediate)
          }
          assert_has_required_fields(msg)
          assert_equal(IDENTIFIER_SELECT,
                       msg.get_arg(OPENID1_NS, 'identity'))
        end

      end

      class TestCheckIDRequestOpenID1Immediate < TestCheckIDRequestOpenID1
        def immediate
          true
        end

        def expected_mode
          'checkid_immediate'
        end
      end

      class TestCheckid_RequestOpenID2Immediate < TestCheckIDRequestOpenID2
        def immediate
          true
        end

        def expected_mode
          'checkid_immediate'
        end
      end
    end
  end
end
