require 'pathname'
require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
require 'ostruct'

TODAY = Date.today
NOW   = DateTime.now

TIME_STRING_1 = '2007-04-21 04:14:12'
TIME_STRING_2 = '2007-04-21 04:14:12.1'
TIME_STRING_3 = '2007-04-21 04:14:12.01'
TIME_STRING_4 = '2007-04-21 04:14:12.123456'

TIME_1 = Time.parse(TIME_STRING_1)
TIME_2 = Time.parse(TIME_STRING_2)
TIME_3 = Time.parse(TIME_STRING_3)
TIME_4 = Time.parse(TIME_STRING_4)

class EveryType
  include DataMapper::Resource

  property :serial,        Serial
  property :fixnum,        Integer,    :nullable => false, :default => 1
  property :string,        String,     :nullable => false, :default => 'default'
  property :empty,         String,     :nullable => false, :default => ''
  property :date,          Date,       :nullable => false, :default => TODAY,                                           :index => :date_date_time, :unique_index => :date_float
  property :true_class,    TrueClass,  :nullable => false, :default => true
  property :false_class,   TrueClass,  :nullable => false, :default => false
  property :text,          DM::Text,   :nullable => false, :default => 'text'
#  property :class,         Class,      :nullable => false, :default => Class  # FIXME: Class types cause infinite recursions in Resource
  property :big_decimal,   BigDecimal, :nullable => false, :default => BigDecimal('1.1'), :precision => 2, :scale => 1
  property :float,         Float,      :nullable => false, :default => 1.1,               :precision => 2, :scale => 1, :unique_index => :date_float
  property :date_time,     DateTime,   :nullable => false, :default => NOW,                                             :index => [:date_date_time, true]
  property :time_1,        Time,       :nullable => false, :default => TIME_1,                                          :unique_index => true
  property :time_2,        Time,       :nullable => false, :default => TIME_2
  property :time_3,        Time,       :nullable => false, :default => TIME_3
  property :time_4,        Time,       :nullable => false, :default => TIME_4
  property :object,        Object,     :nullable => true                       # FIXME: cannot supply a default for Object
  property :discriminator, DM::Discriminator
end

module Publications
  class ShortStoryCollection
    include DataMapper::Resource
    property :serial, Serial
    property :date,   Date,    :nullable => false, :default => TODAY, :index => :date_date_time
  end
end

if HAS_SQLITE3
  describe DataMapper::AutoMigrations, '.auto_migrate! with sqlite3' do
    before :all do
      @adapter = repository(:sqlite3).adapter

      DataMapper::Resource.descendants.clear

      @property_class = Struct.new(:name, :type, :nullable, :default, :serial)
    end

    after :all do
      DataMapper::Resource.descendants.clear
    end

    describe 'with sqlite3' do
      before :all do
        EveryType.auto_migrate!(:sqlite3).should be_true

        @table_set = @adapter.query('PRAGMA table_info(?)', 'every_types').inject({}) do |ts,column|
          default = if 'NULL' == column.dflt_value || column.dflt_value.nil?
            nil
          else
            /^(['"]?)(.*)\1$/.match(column.dflt_value)[2]
          end

          property = @property_class.new(
            column.name,
            column.type.upcase,
            column.notnull == 0,
            default,
            column.pk == 1  # in SQLite3 the serial key is also primary
          )

          ts.update(property.name => property)
        end

        @index_list = @adapter.query('PRAGMA index_list(?)', 'every_types')

        # bypass DM to create the record using only the column default values
        @adapter.execute('INSERT INTO "every_types" ("serial", "discriminator") VALUES (?, ?)', 1, EveryType)

        @book = repository(:sqlite3) { EveryType.first }
      end

      types = {
        :serial        => [ Integer,           'INTEGER',      false, nil,                               1,                 true  ],
        :fixnum        => [ Integer,           'INTEGER',      false, '1',                               1,                 false ],
        :string        => [ String,            'VARCHAR(50)',  false, 'default',                         'default',         false ],
        :empty         => [ String,            'VARCHAR(50)',  false, '',                                ''       ,         false ],
        :date          => [ Date,              'DATE',         false, TODAY.strftime('%Y-%m-%d'),        TODAY,             false ],
        :true_class    => [ TrueClass,         'BOOLEAN',      false, 't',                               true,              false ],
        :false_class   => [ TrueClass,         'BOOLEAN',      false, 'f',                               false,             false ],
        :text          => [ DM::Text,          'TEXT',         false, 'text',                            'text',            false ],
#        :class         => [ Class,             'VARCHAR(50)',  false, 'Class',                           'Class',           false ],
        :big_decimal   => [ BigDecimal,        'DECIMAL(2,1)', false, '1.1',                             BigDecimal('1.1'), false ],
        :float         => [ Float,             'FLOAT(2,1)',   false, '1.1',                             1.1,               false ],
        :date_time     => [ DateTime,          'DATETIME',     false, NOW.strftime('%Y-%m-%d %H:%M:%S'), NOW,               false ],
        :time_1        => [ Time,              'TIMESTAMP',    false, TIME_STRING_1,                     TIME_1,            false ],
#SQLite pads out the microseconds to the full 6 digits no matter what the value is - we simply pad up the zeros needed
        :time_2        => [ Time,              'TIMESTAMP',    false, TIME_STRING_2.dup << '00000',      TIME_2,            false ],
        :time_3        => [ Time,              'TIMESTAMP',    false, TIME_STRING_3.dup << '0000',       TIME_3,            false ],
        :time_4        => [ Time,              'TIMESTAMP',    false, TIME_STRING_4,                     TIME_4,            false ],
        :object        => [ Object,            'TEXT',         true,  nil,                               nil,               false ],
        :discriminator => [ DM::Discriminator, 'VARCHAR(50)',  false, nil,                               EveryType,         false ],
      }

      types.each do |name,(klass,type,nullable,default,key)|
        describe "a #{klass} property" do
          it "should be created as a #{type}" do
            @table_set[name.to_s].type.should == type
          end

          it "should #{!nullable && 'not'} be nullable".squeeze(' ') do
            @table_set[name.to_s].nullable.should == nullable
          end

          it "should have a default value #{default.inspect}" do
            @table_set[name.to_s].default.should == default
          end

          expected_value = types[name][4]
          it 'should properly typecast value' do
            if DateTime == klass
              @book.send(name).to_s.should == expected_value.to_s
            else
              @book.send(name).should == expected_value
            end
          end
        end
      end

      it 'should have 4 indexes: 2 non-unique index, 2 unique index' do
        @index_list.size.should == 4

        expected_indices = {
          "unique_index_every_types_date_float" => 1,
          "unique_index_every_types_time_1" => 1,
          "index_every_types_date_date_time" => 0,
          "index_every_types_date_time" => 0
        }

        @index_list.each do |index|
          expected_indices.should have_key(index.name)
          expected_indices[index.name].should == index.unique
        end
      end

      it 'should escape a namespaced model' do
        Publications::ShortStoryCollection.auto_migrate!(:sqlite3).should be_true
        @adapter.query('SELECT "name" FROM "sqlite_master" WHERE type = ?', 'table').should include('publications_short_story_collections')
      end
    end
  end
end

if HAS_MYSQL
  describe DataMapper::AutoMigrations, '.auto_migrate! with mysql' do
    before :all do
      @adapter = repository(:mysql).adapter

      DataMapper::Resource.descendants.clear

      @property_class = Struct.new(:name, :type, :nullable, :default, :serial)
    end

    after :all do
      DataMapper::Resource.descendants.clear
    end

    describe 'with mysql' do#
      before :all do
        EveryType.auto_migrate!(:mysql).should be_true

        @table_set = @adapter.query('DESCRIBE `every_types`').inject({}) do |ts,column|
          property = @property_class.new(
            column.field,
            column.type.upcase,
            column.null == 'YES',
            column.type.upcase == 'TEXT' ? nil : column.default,
            column.extra.split.include?('auto_increment')
          )

          ts.update(property.name => property)
        end

        @index_list = @adapter.query('SHOW INDEX FROM `every_types`')

        # bypass DM to create the record using only the column default values
        @adapter.execute('INSERT INTO `every_types` (`serial`, `text`, `discriminator`) VALUES (?, ?, ?)', 1, 'text', EveryType)

        @book = repository(:mysql) { EveryType.first }
      end

      types = {
        :serial        => [ Integer,           'INT(11)',      false, nil,                                  1,                 true  ],
        :fixnum        => [ Integer,           'INT(11)',      false, '1',                                  1,                 false ],
        :string        => [ String,            'VARCHAR(50)',  false, 'default',                            'default',         false ],
        :empty         => [ String,            'VARCHAR(50)',  false, '',                                   '',                false ],
        :date          => [ Date,              'DATE',         false, TODAY.strftime('%Y-%m-%d'),           TODAY,             false ],
        :true_class    => [ TrueClass,         'TINYINT(1)',   false, '1',                                  true,              false ],
        :false_class   => [ TrueClass,         'TINYINT(1)',   false, '0',                                  false,             false ],
        :text          => [ DM::Text,          'TEXT',         false, nil,                                  'text',            false ],
#        :class         => [ Class,             'VARCHAR(50)',  false, 'Class',                              'Class',           false ],
        :big_decimal   => [ BigDecimal,        'DECIMAL(2,1)', false, '1.1',                                BigDecimal('1.1'), false ],
        :float         => [ Float,             'FLOAT(2,1)',   false, '1.1',                                1.1,               false ],
        :date_time     => [ DateTime,          'DATETIME',     false, NOW.strftime('%Y-%m-%d %H:%M:%S'),    NOW,               false ],
        :time_1        => [ Time,              'TIMESTAMP',    false, TIME_1.strftime('%Y-%m-%d %H:%M:%S'), TIME_1,            false ],
        :time_2        => [ Time,              'TIMESTAMP',    false, TIME_2.strftime('%Y-%m-%d %H:%M:%S'), TIME_2,            false ],
        :time_3        => [ Time,              'TIMESTAMP',    false, TIME_3.strftime('%Y-%m-%d %H:%M:%S'), TIME_3 ,           false ],
        :time_4        => [ Time,              'TIMESTAMP',    false, TIME_4.strftime('%Y-%m-%d %H:%M:%S'), TIME_4 ,           false ],
        :object        => [ Object,            'TEXT',         true,  nil,                                  nil,               false ],
        :discriminator => [ DM::Discriminator, 'VARCHAR(50)',  false, nil,                                  EveryType,         false ],
      }

      types.each do |name,(klass,type,nullable,default,key)|
        describe "a #{klass} property" do
          it "should be created as a #{type}" do
            @table_set[name.to_s].type.should == type
          end

          it "should #{!nullable && 'not'} be nullable".squeeze(' ') do
            @table_set[name.to_s].nullable.should == nullable
          end

          it "should have a default value #{default.inspect}" do
            @table_set[name.to_s].default.should == default
          end

          expected_value = types[name][4]
          it 'should properly typecast value' do
            if DateTime == klass || Time == klass # mysql doesn't support microsecond
              @book.send(name).to_s.should == expected_value.to_s
            else
              @book.send(name).should == expected_value
            end
          end
        end
      end

      it 'should have 4 indexes: 2 non-unique index, 2 unique index' do
        pending do
          # TODO
          @index_list[0].Key_name.should == 'unique_index_every_types_date_float'
          @index_list[0].Non_unique.should == 0
          @index_list[1].Key_name.should == 'unique_index_every_types_time_1'
          @index_list[1].Non_unique.should == 0
          @index_list[2].Key_name.should == 'index_every_types_date_date_time'
          @index_list[2].Non_unique.should == 1
          @index_list[3].Key_name.should == 'index_every_types_date_time'
          @index_list[3].Non_unique.should == 1
        end
      end

      it 'should escape a namespaced model' do
        Publications::ShortStoryCollection.auto_migrate!(:mysql).should be_true
        @adapter.query('SHOW TABLES').should include('publications_short_story_collections')
      end
    end
  end
end

if HAS_POSTGRES
  describe DataMapper::AutoMigrations, '.auto_migrate! with postgres' do
    before :all do
      @adapter = repository(:postgres).adapter

      DataMapper::Resource.descendants.clear

      @property_class = Struct.new(:name, :type, :nullable, :default, :serial)
    end

    after :all do
      DataMapper::Resource.descendants.clear
    end

    describe 'with postgres' do
      before :all do
        EveryType.auto_migrate!(:postgres).should be_true

        query = <<-EOS
          SELECT
            -- Field
              "pg_attribute"."attname" AS "Field",
            -- Type
              CASE "pg_type"."typname"
                WHEN 'varchar' THEN 'varchar'
                ELSE "pg_type"."typname"
              END AS "Type",
            -- Null
              CASE WHEN "pg_attribute"."attnotnull" THEN ''
                ELSE 'YES'
              END AS "Null",
            -- Default
              "pg_attrdef"."adsrc" AS "Default"
          FROM "pg_class"
            INNER JOIN "pg_attribute"
              ON ("pg_class"."oid" = "pg_attribute"."attrelid")
            INNER JOIN pg_type
              ON ("pg_attribute"."atttypid" = "pg_type"."oid")
            LEFT JOIN "pg_attrdef"
              ON ("pg_class"."oid" = "pg_attrdef"."adrelid" AND "pg_attribute"."attnum" = "pg_attrdef"."adnum")
          WHERE "pg_class"."relname" = ? AND "pg_attribute"."attnum" >= ? AND NOT "pg_attribute"."attisdropped"
          ORDER BY "pg_attribute"."attnum"
        EOS

        @table_set = @adapter.query(query, 'every_types', 1).inject({}) do |ts,column|
          default = column.default
          serial  = false

          if column.default == "nextval('every_types_serial_seq'::regclass)"
            default = nil
            serial  = true
          end

          property = @property_class.new(
            column.field,
            column.type.upcase,
            column.null == 'YES',
            default,
            serial
          )

          ts.update(property.name => property)
        end

        # bypass DM to create the record using only the column default values
        @adapter.execute('INSERT INTO "every_types" ("serial", "discriminator") VALUES (?, ?)', 1, EveryType)

        @book = repository(:postgres) { EveryType.first }
      end

      types = {
        :serial        => [ Integer,           'INT4',        false, nil,                                                                   1,                 true  ],
        :fixnum        => [ Integer,           'INT4',        false, '1',                                                                   1,                 false ],
        :string        => [ String,            'VARCHAR',     false, "'default'::character varying",                                        'default',         false ],
        :empty         => [ String,            'VARCHAR',     false, "''::character varying",                                               '',                false ],
        :date          => [ Date,              'DATE',        false, "'#{TODAY.strftime('%Y-%m-%d')}'::date",                               TODAY,             false ],
        :true_class    => [ TrueClass,         'BOOL',        false, 'true',                                                                true,              false ],
        :false_class   => [ TrueClass,         'BOOL',        false, 'false',                                                               false,             false ],
        :text          => [ DM::Text,          'TEXT',        false, "'text'::text",                                                        'text',            false ],
#        :class         => [ Class,             'VARCHAR(50)', false, 'Class',                                                               'Class',           false ],
        :big_decimal   => [ BigDecimal,        'NUMERIC',     false, '1.1',                                                                 BigDecimal('1.1'), false ],
        :float         => [ Float,             'FLOAT8',      false, '1.1',                                                                 1.1,               false ],
        :date_time     => [ DateTime,          'TIMESTAMP',   false, "'#{NOW.strftime('%Y-%m-%d %H:%M:%S')}'::timestamp without time zone", NOW,               false ],
        :time_1        => [ Time,              'TIMESTAMP',   false, "'" << TIME_STRING_1.dup << "'::timestamp without time zone",          TIME_1,            false ],
#The weird zero here is simply because postgresql seems to want to store .10 instead of .1 for this one
#affects anything with an exact tenth of a second (i.e. .1, .2, .3, ...)
        :time_2        => [ Time,              'TIMESTAMP',   false, "'" << TIME_STRING_2.dup << "0'::timestamp without time zone",         TIME_2,            false ],
        :time_3        => [ Time,              'TIMESTAMP',   false, "'" << TIME_STRING_3.dup << "'::timestamp without time zone",          TIME_3,            false ],
        :time_4        => [ Time,              'TIMESTAMP',   false, "'" << TIME_STRING_4.dup << "'::timestamp without time zone",          TIME_4,            false ],
        :object        => [ Object,            'TEXT',        true,  nil,                                                                   nil,               false ],
        :discriminator => [ DM::Discriminator, 'VARCHAR',     false, nil,                                                                   EveryType,         false ],
      }

      types.each do |name,(klass,type,nullable,default,key)|
        describe "a #{Extlib::Inflection.classify(name.to_s)} property" do
          it "should be created as a #{type}" do
            @table_set[name.to_s].type.should == type
          end

          it "should #{!nullable && 'not'} be nullable".squeeze(' ') do
            @table_set[name.to_s].nullable.should == nullable
          end

          it "should have a default value #{default.inspect}" do
            @table_set[name.to_s].default.should == default
          end

          expected_value = types[name][4]
          it 'should properly typecast value' do
            if DateTime == klass
              @book.send(name).to_s.should == expected_value.to_s
            else
              @book.send(name).should == expected_value
            end
          end
        end
      end

      it 'should have 4 indexes: 2 non-unique index, 2 unique index' do
        pending 'TODO'
      end

      it 'should escape a namespaced model' do
        Publications::ShortStoryCollection.auto_migrate!(:postgres).should be_true
        @adapter.query('SELECT "tablename" FROM "pg_tables" WHERE "tablename" NOT LIKE ?', 'pg_%').should include('publications_short_story_collections')
      end
    end
  end
end
