require 'assert'
require 'mr/read_model/querying'

require 'ardb/relation_spy'
require 'much-plugin'
require 'mr/fake_record'
require 'mr/fake_query'
require 'mr/query'
require 'test/support/read_model/querying'

module MR::ReadModel

  class UnitTests < Assert::Context
    include MR::ReadModel::Querying::TestHelpers
    desc "MR::ReadModel"

  end

  class QueryingTests < UnitTests
    desc "Querying"
    setup do
      @read_model_class = Class.new do
        include MR::ReadModel::Querying
        attr_reader :data
        def initialize(data); @data = data; end
      end
    end
    subject{ @read_model_class }

    should have_imeths :relation, :record_class
    should have_imeths :find, :query, :count, :build_sql, :find_attr
    should have_imeths :select
    should have_imeths :from, :joins
    should have_imeths :where
    should have_imeths :order
    should have_imeths :group, :having
    should have_imeths :limit, :offset
    should have_imeths :merge
    should have_imeths :inner_join_subquery
    should have_imeths :left_outer_join_subquery, :left_join_subquery
    should have_imeths :right_outer_join_subquery, :right_join_subquery
    should have_imeths :full_outer_join_subquery, :full_join_subquery

    should "use much-plugin" do
      assert_includes MuchPlugin, MR::ReadModel::Querying
    end

    should "return a relation using `relation`" do
      relation = subject.relation
      assert_instance_of MR::ReadModel::Relation, relation
      assert_same relation, subject.relation
    end

    should "know its record class" do
      if Factory.boolean
        subject.from FakeTestRecord
      else
        subject.from_subquery do
          read_model{ from FakeTestRecord }
        end
      end
      assert_equal FakeTestRecord, subject.record_class
    end

    should "know how to find a specific result" do
      build_for_find_called_with = nil
      relation_spy               = Ardb::RelationSpy.new
      Assert.stub(subject.relation, :build_for_find) do |*args|
        build_for_find_called_with = args
        relation_spy
      end

      record = FakeTestRecord.new
      relation_spy.results << record

      id = Factory.integer
      result = subject.find(id)
      assert_equal [id, {}], build_for_find_called_with
      assert_instance_of subject, result
      assert_same record, result.data

      params = { Factory.string => Factory.string }
      result = subject.find(id, params)
      assert_equal [id, params], build_for_find_called_with
      assert_instance_of subject, result
      assert_same record, result.data

      relation_spy.results.clear
      assert_raises{ subject.find(id) }
      assert_raises{ subject.find(id, params) }
    end

    should "know how to build its sql" do
      build_sql_called_with = nil
      built_sql             = Factory.text
      Assert.stub(subject.relation, :build_sql) do |params|
        build_sql_called_with = params
        built_sql
      end

      result = subject.build_sql
      assert_nil build_sql_called_with
      assert_equal built_sql, result

      params = { Factory.string => Factory.string }
      result = subject.build_sql(params)
      assert_equal params, build_sql_called_with
      assert_equal built_sql, result
    end

    should "allow setting the relation's find attr" do
      column = Factory.string
      subject.find_attr column
      assert_equal column, subject.relation.find_attr
      assert_equal column, subject.find_attr
    end

    should "set the relation's from using `from`" do
      from_called_with = nil
      Assert.stub(subject.relation, :from){ |*args| from_called_with = args }

      subject.from FakeTestRecord
      assert_equal [FakeTestRecord], from_called_with
    end

    should "set the relation's from subquery using `from_subquery`" do
      from_subquery_called_with = nil
      Assert.stub(subject.relation, :from_subquery) do |&block|
        from_subquery_called_with = block
      end

      subquery_block = proc{ Factory.string }
      subject.from_subquery(&subquery_block)
      assert_equal subquery_block, from_subquery_called_with
    end

    should "raise an argument error when passing `from` a non MR::Record" do
      assert_raises(ArgumentError){ subject.from(Class.new) }
    end

    should "raise an argument error when no block is passed to `from_subquery`" do
      assert_raises(ArgumentError){ subject.from_subquery }
    end

    should "raise a no record class error when using the relation before it's configured" do
      assert_raises(MR::ReadModel::NoFromExpressionError){ subject.query }
    end

  end

  class WithFromRecordClassTests < QueryingTests
    setup do
      @ar_relation_spy = Ardb::RelationSpy.new
      Assert.stub(FakeTestRecord, :scoped){ @ar_relation_spy }
      @read_model_class.from FakeTestRecord
      @relation = @read_model_class.relation
    end

    should "add a static select to the relation with `select`" do
      select_sql = "some_table.some_column AS 'something'"
      subject.select select_sql
      assert_static_expression_added @relation, :select, select_sql
    end

    should "add a dynamic select to the relation with `select`" do
      select_proc = proc{ |name| "some_table.some_column AS '#{name}'" }
      subject.select(&select_proc)
      assert_dynamic_expression_added @relation, :select, select_proc
    end

    should "return the expression using `select`" do
      select_sql = "some_table.some_column AS 'something'"
      expression = subject.select select_sql
      assert_static_expression expression, :select, select_sql
    end

    should "add a static join to the relation with `joins`" do
      join_args = [ :some_table, :other_table ]
      subject.joins(*join_args)
      assert_static_expression_added @relation, :joins, *join_args
    end

    should "add a dynamic join to the relation with `joins`" do
      join_proc = proc{ |name| "CROSS JOIN #{name}" }
      subject.joins(&join_proc)
      assert_dynamic_expression_added @relation, :joins, join_proc
    end

    should "return the expression using `joins`" do
      join_args = [ :some_table, :other_table ]
      expression = subject.joins(*join_args)
      assert_static_expression expression, :joins, join_args
    end

    should "add a static merge to the relation with `where`" do
      merge_args = 'fake-relation'
      subject.where(merge_args)
      assert_static_merge_expression_added @relation, :where, merge_args
    end

    should "add a dynamic merge to the relation with `where`" do
      merge_proc = proc{ 'fake-relation' }
      subject.where(&merge_proc)
      assert_dynamic_merge_expression_added @relation, :where, merge_proc
    end

    should "return the merge expression using `where`" do
      merge_args = 'fake-relation'
      expression = subject.where(merge_args)
      assert_static_merge_expression expression, :where, merge_args
    end

    should "add a static merge to the relation with `order`" do
      merge_args = 'fake-relation'
      subject.order(merge_args)
      assert_static_merge_expression_added @relation, :order, merge_args
    end

    should "add a dynamic merge to the relation with `order`" do
      merge_proc = proc{ 'fake-relation' }
      subject.order(&merge_proc)
      assert_dynamic_merge_expression_added @relation, :order, merge_proc
    end

    should "return the merge expression using `order`" do
      merge_args = 'fake-relation'
      expression = subject.order(merge_args)
      assert_static_merge_expression expression, :order, merge_args
    end

    should "add a static group to the relation with `group`" do
      group_args = 'some_table.some_column'
      subject.group(group_args)
      assert_static_expression_added @relation, :group, group_args
    end

    should "add a dynamic group to the relation with `group`" do
      group_proc = proc{ |column| column }
      subject.group(&group_proc)
      assert_dynamic_expression_added @relation, :group, group_proc
    end

    should "return the expression using `group`" do
      group_args = 'some_table.some_column'
      expression = subject.group(group_args)
      assert_static_expression expression, :group, group_args
    end

    should "add a static having to the relation with `having`" do
      having_args = 'COUNT(*) > 0'
      subject.having(having_args)
      assert_static_expression_added @relation, :having, having_args
    end

    should "add a dynamic having to the relation with `having`" do
      having_proc = proc{ |column| "COUNT(#{column}) > 0" }
      subject.having(&having_proc)
      assert_dynamic_expression_added @relation, :having, having_proc
    end

    should "return the expression using `having`" do
      having_args = 'COUNT(*) > 0'
      expression = subject.having(having_args)
      assert_static_expression expression, :having, having_args
    end

    should "add a static limit to the relation with `limit`" do
      limit_args = 1
      subject.limit(limit_args)
      assert_static_expression_added @relation, :limit, limit_args
    end

    should "add a dynamic limit to the relation with `limit`" do
      limit_proc = proc{ |count| count }
      subject.limit(&limit_proc)
      assert_dynamic_expression_added @relation, :limit, limit_proc
    end

    should "return the expression using `limit`" do
      limit_args = 1
      expression = subject.limit(limit_args)
      assert_static_expression expression, :limit, limit_args
    end

    should "add a static offset to the relation with `offset`" do
      offset_args = 1
      subject.offset(offset_args)
      assert_static_expression_added @relation, :offset, offset_args
    end

    should "add a dynamic offset to the relation with `offset`" do
      offset_proc = proc{ |count| count }
      subject.offset(&offset_proc)
      assert_dynamic_expression_added @relation, :offset, offset_proc
    end

    should "return the expression using `offset`" do
      offset_args = 1
      expression = subject.offset(offset_args)
      assert_static_expression expression, :offset, offset_args
    end

    should "add a static merge to the relation with `merge`" do
      merge_args = 'fake-relation'
      subject.merge(merge_args)
      assert_static_merge_expression_added @relation, :merge, merge_args
    end

    should "add a dynamic merge to the relation with `merge`" do
      merge_proc = proc{ 'fake-relation' }
      subject.merge(&merge_proc)
      assert_dynamic_merge_expression_added @relation, :merge, merge_proc
    end

    should "return the merge expression using `merge`" do
      merge_args = 'fake-relation'
      expression = subject.merge(merge_args)
      assert_static_merge_expression expression, :merge, merge_args
    end

    should "add a join subquery to the relation with `inner_join_subquery`" do
      subquery_proc = proc{ as('my_table') }
      expression = subject.inner_join_subquery(&subquery_proc)
      assert_inner_join_subquery_added @relation, subquery_proc
    end

    should "return the subquery expression using `inner_join_subquery`" do
      subquery_proc = proc{ as('my_table') }
      expression = subject.inner_join_subquery(&subquery_proc)
      assert_join_subquery_expression expression, :inner, subquery_proc
    end

    should "add a join subquery to the relation with `left_outer_join_subquery`" do
      subquery_proc = proc{ as('my_table') }
      expression = subject.left_outer_join_subquery(&subquery_proc)
      assert_left_outer_join_subquery_added @relation, subquery_proc
    end

    should "return the subquery expression using `left_outer_join_subquery`" do
      subquery_proc = proc{ as('my_table') }
      expression = subject.left_outer_join_subquery(&subquery_proc)
      assert_join_subquery_expression expression, :left, subquery_proc
    end

    should "add a join subquery to the relation with `right_outer_join_subquery`" do
      subquery_proc = proc{ as('my_table') }
      expression = subject.right_outer_join_subquery(&subquery_proc)
      assert_right_outer_join_subquery_added @relation, subquery_proc
    end

    should "return the subquery expression using `right_outer_join_subquery`" do
      subquery_proc = proc{ as('my_table') }
      expression = subject.right_outer_join_subquery(&subquery_proc)
      assert_join_subquery_expression expression, :right, subquery_proc
    end

    should "add a join subquery to the relation with `full_outer_join_subquery`" do
      subquery_proc = proc{ as('my_table') }
      expression = subject.full_outer_join_subquery(&subquery_proc)
      assert_full_outer_join_subquery_added @relation, subquery_proc
    end

    should "return the subquery expression using `full_outer_join_subquery`" do
      subquery_proc = proc{ as('my_table') }
      expression = subject.full_outer_join_subquery(&subquery_proc)
      assert_join_subquery_expression expression, :full, subquery_proc
    end

    should "raise an argument error when any query method isn't provided args or a block" do
      assert_raises(ArgumentError){ subject.select }
      assert_raises(ArgumentError){ subject.joins }
      assert_raises(ArgumentError){ subject.where }
      assert_raises(ArgumentError){ subject.order }
      assert_raises(ArgumentError){ subject.group }
      assert_raises(ArgumentError){ subject.having }
      assert_raises(ArgumentError){ subject.limit }
      assert_raises(ArgumentError){ subject.offset }
      assert_raises(ArgumentError){ subject.merge }
      assert_raises(ArgumentError){ subject.inner_join_subquery }
      assert_raises(ArgumentError){ subject.left_outer_join_subquery }
      assert_raises(ArgumentError){ subject.right_outer_join_subquery }
      assert_raises(ArgumentError){ subject.full_outer_join_subquery }
    end

  end

  class QueryTests < WithFromRecordClassTests
    desc "query"
    setup do
      @read_model_class.select(:name)
      @query = @read_model_class.query
    end
    subject{ @query }

    should "return an instance of an MR::Query for the class and relation" do
      assert_instance_of MR::Query, subject
      assert_equal @read_model_class, subject.model_class
      assert_equal @ar_relation_spy,  subject.relation
    end

    should "have applied the query expressions to the relation" do
      assert_expression_applied @ar_relation_spy, :select, :name
    end

  end

  class CountTests < WithFromRecordClassTests
    desc "count"
    setup do
      results = Array.new(Factory.integer(10))
      @query  = MR::FakeQuery.new(results)

      @args = if Factory.boolean
        [{ Factory.string => Factory.string }]
      else # should support when query is overidden
        @read_model_class.class_eval do
          def self.query(something, something_else)
            super({
              :something      => something,
              :something_else => something_else
            })
          end
        end
        [Factory.string, Factory.string]
      end

      @query_called_with = nil
      Assert.stub(@read_model_class, :query) do |*args|
        @query_called_with = args
        @query
      end
    end

    should "know how to return a count for its query" do
      result = subject.count(*@args)
      assert_equal @args,        @query_called_with
      assert_equal @query.count, result
    end

  end

  class RelationTests < UnitTests
    desc "Relation"
    setup do
      @relation_class = MR::ReadModel::Relation
      @relation       = @relation_class.new
    end
    subject{ @relation }

    should have_readers :from_expression, :query_expressions
    should have_writers :find_attr
    should have_imeths :from, :from_subquery, :from_record_class, :find_attr
    should have_imeths :build_for_find, :build_for_all, :build_sql

    should "know its find find excluded types" do
      exp = [:where, :limit, :offset]
      assert_equal exp, @relation_class::FIND_EXCLUDED_TYPES
    end

    should "not have a from expression or any expressions by default" do
      assert_instance_of NullFromExpression, subject.from_expression
      assert_equal [], subject.query_expressions
    end

    should "set its from expression using `from`" do
      subject.from FakeTestRecord

      assert_instance_of FromExpression, subject.from_expression
      assert_equal FakeTestRecord, subject.from_expression.record_class
    end

    should "set its from expression using `from_subquery`" do
      subquery_block = proc{ Factory.string }
      subject.from_subquery(&subquery_block)

      assert_instance_of FromSubqueryExpression, subject.from_expression
      assert_equal subquery_block, subject.from_expression.subquery_block
    end

    should "know how to build its sql" do
      built_sql    = Factory.text
      relation_spy = Ardb::RelationSpy.new
      Assert.stub(relation_spy, :to_sql){ "  #{built_sql}  " }

      build_for_all_called_with = nil
      Assert.stub(subject, :build_for_all) do |params|
        build_for_all_called_with = params
        relation_spy
      end

      result = subject.build_sql
      assert_equal built_sql, result
      assert_nil build_for_all_called_with

      params = { Factory.string => Factory.string }
      result = subject.build_sql(params)
      assert_equal built_sql, result
      assert_equal params,    build_for_all_called_with
    end

  end

  class RelationWithFromTests < RelationTests
    desc "with a from expression"
    setup do
      if Factory.boolean
        @relation.from FakeTestRecord
      else
        @relation.from_subquery do
          read_model{ from FakeTestRecord }
        end
        @relation.find_attr = Factory.string
      end
      @from_ar_relation_built_with = nil
      Assert.stub(@relation.from_expression, :ar_relation) do |params|
        @from_ar_relation_built_with = params
        Ardb::RelationSpy.new
      end
      @relation.query_expressions << QueryExpression.new(:select, Factory.string)
      @relation.query_expressions << QueryExpression.new(:order,  Factory.string)
      @relation_class::FIND_EXCLUDED_TYPES.each do |type|
        @relation.query_expressions << QueryExpression.new(type, Factory.string)
      end
    end

    should "know its from record class" do
      assert_equal subject.from_expression.record_class, subject.from_record_class
    end

    should "know its find attr" do
      relation = @relation_class.new.tap{ |r| r.from FakeTestRecord }
      assert_equal relation.from_expression.default_find_attr, relation.find_attr

      column = Factory.string
      relation.find_attr = column
      assert_equal column, relation.find_attr

      # we don't allow from subquery expressions to default their from attr so
      # the user has to manually specify it
      relation = @relation_class.new
      relation.from_subquery do
        read_model{ from FakeTestRecord }
      end
      assert_raises(NoFindAttrError){ relation.find_attr }

      relation.find_attr = column
      assert_equal column, relation.find_attr
    end

    should "build an activerecord relation for find using `build_for_find`" do
      id = Factory.integer
      ar_relation_spy = subject.build_for_find(id)
      assert_nil @from_ar_relation_built_with
      subject.query_expressions.each do |e|
        if !@relation_class::FIND_EXCLUDED_TYPES.include?(e.type)
          assert_expression_applied ar_relation_spy, e.type, *e.args
        else
          assert_not_expression_applied ar_relation_spy, e.type, *e.args
        end
      end
    end

    should "return a new relation everytime `build_for_find` is called" do
      id = Factory.integer
      ar_relation = subject.build_for_find(id)
      assert_not_same ar_relation, subject.build_for_find(id)
    end

    should "apply expressions using the params passed to `build_for_find`" do
      subject.query_expressions << QueryExpression.new(:select){ |p| p['column'] }

      id     = Factory.integer
      params = { 'column' => Factory.string }
      ar_relation = subject.build_for_find(id, params)
      assert_equal params, @from_ar_relation_built_with
      assert_expression_applied ar_relation, :select, params['column']
    end

    should "return a relation from the record class using `build_for_all`" do
      ar_relation_spy = subject.build_for_all
      assert_nil @from_ar_relation_built_with
      subject.query_expressions.each do |e|
        assert_expression_applied ar_relation_spy, e.type, *e.args
      end
    end

    should "return a new relation everytime `build_for_all` is called" do
      ar_relation = subject.build_for_all
      assert_not_same ar_relation, subject.build_for_all
    end

    should "apply query expressions using the params passed to `build_for_all`" do
      subject.query_expressions << QueryExpression.new(:select){ |p| p['column'] }
      params = { 'column' => Factory.string }
      ar_relation = subject.build_for_all(params)
      assert_equal params, @from_ar_relation_built_with
      assert_expression_applied ar_relation, :select, params['column']
    end

  end

  class FakeTestRecord
    include MR::FakeRecord

    def self.table_name;  @table_name  ||= Factory.string; end
    def self.primary_key; @primary_key ||= Factory.string; end

    def self.scoped
      Ardb::RelationSpy.new
    end
  end

end
