# -*- coding: utf-8 -*-
require File.expand_path('../../test_helper', __FILE__)

module Checkr
  class ApiClassTest < Test::Unit::TestCase

    context 'Non-network actions' do
      setup do
        @mock.expects(:get).never
        @mock.expects(:post).never
        @mock.expects(:put).never
        @mock.expects(:delete).never
      end

      should 'not fetch over the network when creating a new APIClass' do
        MockResource.new('fake_id')
      end

      should 'not fetch over the network when creating a new APIClass from a hash' do
        MockResource.construct(test_mock_resource)
      end

      should 'not fetch over the network when setting an attribute' do
        c = MockResource.new('fake_id')
        c.name = 'Another Name'
      end

      should 'not fetch over the network when accessing an attribute' do
        c = MockResource.new('fake_id')
        c.id
      end
    end

    # These GET, POST, etc should really be in a dif test, but this is easier.
    context 'Making a GET request' do
      should 'urlencode values in params and have no payload' do
        response = test_response(test_mock_resource_list)

        # The test is to basically make sure this expectation passes.
        @mock.expects(:get).with do |url, headers, params|
          (url == "#{Checkr.api_base}#{MockResource.path}?page=1&filter=test%20filter" ||
            url == "#{Checkr.api_base}#{MockResource.path}?filter=test%20filter&page=1") &&
          params == nil
        end.returns(response)

        list = MockResource.all({
          :page => 1,
          :filter => 'test filter',
        })
      end
    end

    context 'Making a DELETE request' do
      should 'urlencode values in params and have no payload' do
        @mock.expects(:get).once.returns(test_response(test_mock_resource))
        mock_resource = MockResource.retrieve("fake_id")

        @mock.expects(:delete).once.with do |url, headers, payload|
          url == "#{Checkr.api_base}#{mock_resource.path}?reason=delinquent%20payments" && payload.nil?
        end.returns(test_response({}))
        mock_resource.delete(:reason => "delinquent payments")
      end
    end

    context 'Making a PUT request' do
      should 'have a payload with no query string' do
        @mock.expects(:get).once.returns(test_response(test_mock_resource))
        mock_resource = MockResource.retrieve("fake_id")
        mock_resource.name = "new name"

        @mock.expects(:put).once.with do |url, header, payload|
          payload == { :name => "new name" }
        end.returns(test_response(test_mock_resource))
        mock_resource.save
      end
    end

    context 'Making a POST request' do
      should 'have a payload with no query string' do
        params = { :name => "some name" }
        @mock.expects(:post).once.with("#{Checkr.api_base}#{MockResource.path}", anything, params).returns(test_response(test_mock_resource))

        MockResource.create(params)
      end
    end

    context 'APIClass :default_params' do
      context 'api_instance_method' do
        setup do
          @response = test_response(test_mock_resource_list)
          @mr = MockResource.new(test_mock_resource)
        end

        should 'call any provided lambdas and set the result as params' do
          @mock.expects(:put).once.with(anything, anything, { :name => "new name" }).returns(@response)

          @mr.name = "new name"
          @mr.with_lambda
        end

        should 'call any method defined by a symbol and set the results as params' do
          @mock.expects(:put).once.with(anything, anything, { :name => "new name" }).returns(@response)

          @mr.name = "new name"
          @mr.with_symbol
        end

        context 'order priority' do
          # Intended order of priority:
          # 1. Params. eg in mock_resource.save(params, opts), params should take
          #    priority with duplicate keys.
          # 2. Arguments. These should take priority over default values from :params
          #    method.
          # 3. Results from method defined by :default_params. This can be a symbol that is
          #    the name of a method to call, or a lambda (though a hash is preferred).
          should 'prioritize params argument over args and default_params method' do
            params = {
              :name => "not arg name",
              :tarray => ["not method tarray"],
              :thash => { :val => "custom hash" }
            }
            @mock.expects(:put).once.with(anything, anything, params).returns(@response)

            @mr.tarray = ["method array"]
            @mr.with_symbol_and_args("arg name", params)
          end

          should 'prioritize arguments over :default_params' do
            @mock.expects(:put).once.with(anything, anything, { :name => "arg name" }).returns(@response)

            @mr.name = "changed name"
            @mr.with_symbol_and_args("arg name")
          end

          should 'use :default_params method as even when args and params are present' do
            @mock.expects(:put).once.with(anything, anything, {
              :thash => { :val => "params hash" },
              :name => "arg name",
              :tarray => ["change array"],
            }).returns(@response)

            @mr.tarray = ["change array"]
            @mr.with_symbol_and_args("arg name", { :thash => { :val => "params hash" } })
          end
        end
      end

      context 'api_class_method' do
        setup do
          @response = test_response(test_mock_resource_list)
        end

        should 'call any provided lambdas and set the result as params' do
          @mock.expects(:post).once.with(anything, anything, MockResource.default_values).returns(@response)
          MockResource.with_lambda
        end

        should 'call any method defined by a symbol and set the results as params' do
          @mock.expects(:post).once.with(anything, anything, MockResource.default_values).returns(@response)
          MockResource.with_symbol
        end

        context 'order priority' do
          # Intended order of priority:
          # 1. Params. eg in mock_resource.save(params, opts), params should take
          #    priority with duplicate keys.
          # 2. Arguments. These should take priority over default values from :params
          #    method.
          # 3. Results from method defined by :default_params. This can be a symbol that is
          #    the name of a method to call, or a lambda (though a hash is preferred).
          should 'prioritize params argument over args and default_params method' do
            params = {
              :name => "not arg name",
              :tarray => ["not method tarray"],
              :thash => { :val => "custom hash" }
            }
            @mock.expects(:post).once.with(anything, anything, params).returns(@response)
            MockResource.with_symbol_and_args("name arg", params)
          end

          should 'prioritize arguments over :default_params' do
            params = Util.sorta_deep_clone(MockResource.default_values)
            params[:name] = "name arg"
            @mock.expects(:post).once.with(anything, anything, params).returns(@response)
            MockResource.with_symbol_and_args("name arg", params)
          end

          should 'use :default_params method as even when args and params are present' do
            params = {
              :name => "name arg",
              :tarray => MockResource.default_values[:tarray].dup,
              :thash => { :val => "custom hash" }
            }
            @mock.expects(:post).once.with(anything, anything, params).returns(@response)
            MockResource.with_symbol_and_args("name arg", { :thash => { :val => "custom hash" }})
          end
        end
      end
    end


    context 'APIClass#attribute' do
      should 'create a getter method' do
        assert(MockResource.method_defined?(:name))
      end

      should 'create a setter method' do
        assert(MockResource.method_defined?(:name=))
      end

      should 'have no changed attributes after init' do
        mr = MockResource.new(test_mock_resource)
        assert(mr.changed_attributes.empty?)
      end

      should 'keep track of changed attributes' do
        mr = MockResource.new(test_mock_resource)
        assert(mr.changed_attributes.empty?)
        mr.name = "new name"
        assert_equal({:name => "new name"}, mr.changed_attributes)
      end

      should 'keep track of changed arrays' do
        mr = MockResource.new(test_mock_resource)
        assert(mr.changed_attributes.empty?)
        mr.tarray << "new"
        assert_equal({:tarray => test_mock_resource[:tarray] + ["new"]}, mr.changed_attributes)
      end

      should 'keep track of changed hashes' do
        mr = MockResource.new(test_mock_resource)
        assert(mr.changed_attributes.empty?)
        mr.thash[:some_key] = "new value"
        assert_equal({:thash => { :some_key => "new value" }}, mr.changed_attributes)
      end

      context 'constructors' do
        should 'instantiate on #new' do
          mr = MockResource.new(test_mock_resource)
          assert(mr.nested.is_a?(NestedResource))
          assert(mr.nested_alt.is_a?(NestedResource))
          assert(mr.nested_with.is_a?(NestedWithParent))
          assert_equal(mr.path + "/nested_path", mr.nested_with.path)
        end

        should 'instantiate on #construct' do
          mr = MockResource.construct(test_mock_resource)
          assert(mr.nested.is_a?(NestedResource))
          assert(mr.nested_alt.is_a?(NestedResource))
          assert(mr.nested_with.is_a?(NestedWithParent))
          assert_equal(mr.path + "/nested_path", mr.nested_with.path)
        end

        should 'instantiate on #refresh_from' do
          mr = MockResource.new('fake_id')
          assert(mr.nested.nil?)
          mr.refresh_from(test_mock_resource)
          assert(mr.nested.is_a?(NestedResource))
          assert(mr.nested_alt.is_a?(NestedResource))
          assert(mr.nested_with.is_a?(NestedWithParent))
          assert_equal(mr.path + "/nested_path", mr.nested_with.path)
        end

        should 'with default values' do
          mr = MockResource.new('fake_id')
          assert(mr.nested_with.is_a?(NestedWithParent))
          assert_equal(mr.path + "/nested_path", mr.nested_with.path)
        end
      end
    end

    context 'APIClass :constructor' do
      context 'for api_class_method' do
        setup do
          @mock.expects(:get).once.returns(test_response(test_mock_resource))
        end

        should 'create a new MockResource with :constructor => :self' do
          mr = MockResource.with_con_self
          assert(mr.is_a?(MockResource))
        end

        should 'create a new MockResource with :constructor => MockResource' do
          mr = MockResource.with_con_class
          assert(mr.is_a?(MockResource))
        end

        should 'use the lambda with :constructor => lambda{...}' do
          mr = MockResource.with_con_lambda
          assert_equal("lamdba result", mr)
        end

        should 'create a new MockResource with no constructor defined' do
          mr = MockResource.with_con_default
          assert(mr.is_a?(MockResource))
        end
      end

      context 'for api_instance_method' do
        setup do
          @mock.expects(:get).once.returns(test_response(test_mock_resource))
          @mr = MockResource.new('fake_id')
        end

        should 'update this instance with :constructor => :self' do
          @mr.with_con_self
          assert(@mr.is_a?(MockResource))
          assert_equal(test_mock_resource[:id], @mr.id)
        end

        should 'create a new instance with :constructor => MockResource' do
          res = @mr.with_con_class
          assert(res.is_a?(MockResource))
          assert_not_equal(@mr.id, res.id)
        end

        should 'use the lambda with :constructor => lambda{...}' do
          res = @mr.with_con_lambda
          assert_equal("lamdba result", res)
        end

        should 'update this instance with no constructor defined' do
          @mr.with_con_default
          assert(@mr.is_a?(MockResource))
          assert_equal(test_mock_resource[:id], @mr.id)
        end
      end
    end

    context 'APIClass api_*_method arguments' do
      should 'throw an ArgumentError if too few arguments are provided' do
        assert_raises(ArgumentError) { MockResource.retrieve }
      end

      should 'throw an ArgumentError with the name of missing arguments' do
        begin
          MockResource.retrieve
          assert(false, "ArgumentError was expected.")
        rescue ArgumentError => e
          assert(e.message =~ /id/)
        end
      end

      should 'throw an ArgumentError if too many arguments are provided' do
        assert_raises(ArgumentError) { MockResource.retrieve(1, 2, {}, {}) }
      end

      should 'throw an ArgumentError if the param argument is invalid' do
        assert_raises(ArgumentError) { MockResource.retrieve(1, 2) }
      end

      should 'throw an ArgumentError if the opts argument is invalid' do
        assert_raises(ArgumentError) { MockResource.retrieve(1, {}, "abc") }
      end

      should 'not throw an ArgumentError if the opts or params are nil' do
        @mock.expects(:get).times(2).returns(test_response(test_mock_resource))
        MockResource.retrieve(1, nil, {})
        MockResource.retrieve(1, {}, nil)
      end

      should 'urlencode unused arguments via GET' do
        response = test_response(test_mock_resource)
        @mock.expects(:get).with do |url, headers, params|
          (url == "#{Checkr.api_base}#{MockResource.path}/bval/many?a=aval&c=cval" ||
            url == "#{Checkr.api_base}#{MockResource.path}/bval/many?c=cval&a=aval") &&
          params.nil?
        end.returns(response)

        MockResource.many_args_get("aval", "bval", "cval")
      end

      should 'assign unused arguments to the params payload via POST' do
        response = test_response(test_mock_resource)
        @mock.expects(:post).with("#{Checkr.api_base}#{MockResource.path}/bval/many", anything, { :a => "aval", :c => "cval" }).returns(response)

        MockResource.many_args_post("aval", "bval", "cval")
      end
    end

    context 'APIClass api_instance_method paths' do
      should 'use the provided argument if it is present' do
        response = test_response(test_mock_resource)
        @mock.expects(:get).with("#{Checkr.api_base}/custom_path", anything, anything).returns(response)

        mr = MockResource.new(test_mock_resource)
        mr.custom_path("/custom_path")
      end

      should 'use the instance method for path values' do
        response = test_response(test_mock_resource)
        name = "/mock_resource_name"
        @mock.expects(:get).with("#{Checkr.api_base}#{name}", anything, anything).returns(response)

        mr = MockResource.new(test_mock_resource)
        mr.name = name
        mr.name_path
      end

      should 'use the class method for path values' do
        response = test_response(test_mock_resource)
        @mock.expects(:get).with("#{Checkr.api_base}#{MockResource.crazy}", anything, anything).returns(response)

        mr = MockResource.new(test_mock_resource)
        mr.crazy_path
      end
    end

    context 'APIClass api_class_method paths' do
      should 'use the provided argument if it is present' do
        response = test_response(test_mock_resource)
        @mock.expects(:get).with("#{Checkr.api_base}#{MockResource.path}/fake_id", anything, anything).returns(response)

        mr = MockResource.retrieve('fake_id')
      end

      should 'use the class method for path values' do
        response = test_response(test_mock_resource)
        @mock.expects(:get).with("#{Checkr.api_base}#{MockResource.crazy}", anything, anything).returns(response)

        MockResource.crazy_path
      end
    end

  end
end