# -*- coding: utf-8 -*-
require File.join(File.dirname(__FILE__), 'spec_helper')

describe SelectableAttr do

  def assert_product_discount(klass)
    # productsテーブルのデータから安売り用の価格は
    # product_type_cd毎に決められた割合をpriceにかけて求めます。
    p1 = klass.new(:name => '実践Rails', :product_type_cd => '01', :price => 3000)
    p1.discount_price.should == 2400
    p2 = klass.new(:name => '薔薇の名前', :product_type_cd => '02', :price => 1500)
    p2.discount_price.should == 300
    p3 = klass.new(:name => '未来派野郎', :product_type_cd => '03', :price => 3000)
    p3.discount_price.should == 1500
  end
  
  # 定数をガンガン定義した場合
  # 大文字が多くて読みにくいし、関連するデータ(ここではDISCOUNT)が増える毎に定数も増えていきます。
  class LegacyProduct1 < ActiveRecord::Base
    set_table_name 'products'
    
    PRODUCT_TYPE_BOOK = '01'
    PRODUCT_TYPE_DVD = '02'
    PRODUCT_TYPE_CD = '03'
    PRODUCT_TYPE_OTHER = '09'
    
    PRODUCT_TYPE_OPTIONS = [
      ['書籍', PRODUCT_TYPE_BOOK],
      ['DVD', PRODUCT_TYPE_DVD],
      ['CD', PRODUCT_TYPE_CD],
      ['その他', PRODUCT_TYPE_OTHER]
    ]
    
    DISCOUNT = { 
      PRODUCT_TYPE_BOOK => 0.8,
      PRODUCT_TYPE_DVD => 0.2,
      PRODUCT_TYPE_CD => 0.5,
      PRODUCT_TYPE_OTHER => 1
    }
    
    def discount_price
      (DISCOUNT[product_type_cd] * price).to_i
    end
  end
  
  it "test_legacy_product" do
    assert_product_discount(LegacyProduct1)
    
    # 選択肢を表示するためのデータは以下のように取得できる
    LegacyProduct1::PRODUCT_TYPE_OPTIONS.should ==
      [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
  end
  
  
  
  
  # できるだけ定数定義をまとめた場合
  # 結構すっきりするけど、同じことをいろんなモデルで書くかと思うと気が重い。
  class LegacyProduct2 < ActiveRecord::Base
    set_table_name 'products'
    
    PRODUCT_TYPE_DEFS = [
      {:id => '01', :name => '書籍', :discount => 0.8},
      {:id => '02', :name => 'DVD', :discount => 0.2},
      {:id => '03', :name => 'CD', :discount => 0.5},
      {:id => '09', :name => 'その他', :discount => 1}
    ]
    
    PRODUCT_TYPE_OPTIONS = PRODUCT_TYPE_DEFS.map{|t| [t[:name], t[:id]]}
    DISCOUNT = PRODUCT_TYPE_DEFS.inject({}){|dest, t| 
      dest[t[:id]] = t[:discount]; dest}
      
    def discount_price
      (DISCOUNT[product_type_cd] * price).to_i
    end
  end
  
  it "test_legacy_product" do
    assert_product_discount(LegacyProduct2)
    
    # 選択肢を表示するためのデータは以下のように取得できる
    LegacyProduct2::PRODUCT_TYPE_OPTIONS.should == 
      [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
  end
  
  # selectable_attrを使った場合
  # 定義は一カ所にまとめられて、任意の属性(ここでは:discount)も一緒に書くことができてすっきり〜
  class Product1 < ActiveRecord::Base
    set_table_name 'products'
    
    selectable_attr :product_type_cd do
      entry '01', :book, '書籍', :discount => 0.8
      entry '02', :dvd, 'DVD', :discount => 0.2
      entry '03', :cd, 'CD', :discount => 0.5
      entry '09', :other, 'その他', :discount => 1
    end

    def discount_price
      (product_type_entry[:discount] * price).to_i
    end
  end
  
  it "test_product1" do
    assert_product_discount(Product1)
    # 選択肢を表示するためのデータは以下のように取得できる
    Product1.product_type_options.should ==
      [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
  end
  
  
  # selectable_attrが定義するインスタンスメソッドの詳細
  it "test_product_type_instance_methods" do
    p1 = Product1.new
    p1.product_type_cd.should be_nil
    p1.product_type_key.should be_nil
    p1.product_type_name.should be_nil
    # idを変更すると得られるキーも名称も変わります
    p1.product_type_cd = '02'
    p1.product_type_cd.should ==   '02' 
    p1.product_type_key.should ==  :dvd 
    p1.product_type_name.should == 'DVD'
    # キーを変更すると得られるidも名称も変わります
    p1.product_type_key = :book
    p1.product_type_cd.should ==   '01'
    p1.product_type_key.should ==  :book 
    p1.product_type_name.should == '書籍'
    # id、キー、名称以外の任意の属性は、entryの[]メソッドで取得します。
    p1.product_type_key = :cd
    p1.product_type_entry[:discount].should == 0.5
  end
  
  # selectable_attrが定義するクラスメソッドの詳細
  it "test_product_type_class_methods" do
    # キーからid、名称を取得できます
    Product1.product_type_id_by_key(:book).should == '01'
    Product1.product_type_id_by_key(:dvd).should == '02'
    Product1.product_type_id_by_key(:cd).should == '03'
    Product1.product_type_id_by_key(:other).should == '09'
    Product1.product_type_name_by_key(:book).should == '書籍'
    Product1.product_type_name_by_key(:dvd).should == 'DVD'   
    Product1.product_type_name_by_key(:cd).should == 'CD'    
    Product1.product_type_name_by_key(:other).should == 'その他'
    # 存在しないキーの場合はnilを返します
    Product1.product_type_id_by_key(nil).should be_nil
    Product1.product_type_name_by_key(nil).should be_nil
    Product1.product_type_id_by_key(:unexist).should be_nil
    Product1.product_type_name_by_key(:unexist).should be_nil

    # idからキー、名称を取得できます
    Product1.product_type_key_by_id('01').should == :book
    Product1.product_type_key_by_id('02').should == :dvd
    Product1.product_type_key_by_id('03').should == :cd
    Product1.product_type_key_by_id('09').should == :other
    Product1.product_type_name_by_id('01').should == '書籍'
    Product1.product_type_name_by_id('02').should == 'DVD'
    Product1.product_type_name_by_id('03').should == 'CD'
    Product1.product_type_name_by_id('09').should == 'その他'
    # 存在しないidの場合はnilを返します
    Product1.product_type_key_by_id(nil).should be_nil
    Product1.product_type_name_by_id(nil).should be_nil
    Product1.product_type_key_by_id('99').should be_nil
    Product1.product_type_name_by_id('99').should be_nil
    
    # id、キー、名称の配列を取得できます
    Product1.product_type_ids.should == ['01', '02', '03', '09']
    Product1.product_type_keys.should == [:book, :dvd, :cd, :other]
    Product1.product_type_names.should == ['書籍', 'DVD', 'CD', 'その他']
    # 一部のものだけ取得することも可能です。
    Product1.product_type_ids(:cd, :dvd).should == ['03', '02' ]
    Product1.product_type_keys('02', '03').should == [:dvd, :cd  ]
    Product1.product_type_names('02', '03').should == ['DVD', 'CD'] 
    Product1.product_type_names(:cd, :dvd).should == ['CD', 'DVD']
    
    # select_tagなどのoption_tagsを作るための配列なんか一発っす
    Product1.product_type_options.should == 
      [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
  end

  # selectable_attrのエントリ名をDB上に保持するためのモデル
  class ItemMaster < ActiveRecord::Base
  end
  
  # selectable_attrを使った場合その2
  # アクセス時に毎回アクセス時にDBから項目名を取得します。
  class ProductWithDB1 < ActiveRecord::Base
    set_table_name 'products'
    
    selectable_attr :product_type_cd do
      update_by(
        "select item_cd, name from item_masters where category_name = 'product_type_cd' order by item_no", 
        :when => :everytime)
      entry '01', :book, '書籍', :discount => 0.8
      entry '02', :dvd, 'DVD', :discount => 0.2
      entry '03', :cd, 'CD', :discount => 0.5
      entry '09', :other, 'その他', :discount => 1
    end

    def discount_price
      (product_type_entry[:discount] * price).to_i
    end
  end
  
  it "test_update_entry_name" do
    # DBに全くデータがなくてもコードで記述してあるエントリは存在します。
    ItemMaster.delete_all("category_name = 'product_type_cd'")
    ProductWithDB1.product_type_entries.length.should == 4
    ProductWithDB1.product_type_name_by_key(:book).should == '書籍'
    ProductWithDB1.product_type_name_by_key(:dvd).should == 'DVD'
    ProductWithDB1.product_type_name_by_key(:cd).should == 'CD'
    ProductWithDB1.product_type_name_by_key(:other).should == 'その他'

    assert_product_discount(ProductWithDB1)
    
    # DBからエントリの名称を動的に変更できます
    item_book = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 1, :item_cd => '01', :name => '本')
    ProductWithDB1.product_type_entries.length.should == 4
    ProductWithDB1.product_type_name_by_key(:book).should == '本'    
    ProductWithDB1.product_type_name_by_key(:dvd).should == 'DVD'   
    ProductWithDB1.product_type_name_by_key(:cd).should == 'CD'    
    ProductWithDB1.product_type_name_by_key(:other).should == 'その他' 
    ProductWithDB1.product_type_options.should == 
      [['本', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
    
    # DBからエントリの並び順を動的に変更できます
    item_book.item_no = 4;
    item_book.save!
    item_other = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 1, :item_cd => '09', :name => 'その他')
    item_dvd = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 2, :item_cd => '02') # nameは指定しなかったらデフォルトが使われます。
    item_cd = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 3, :item_cd => '03') # nameは指定しなかったらデフォルトが使われます。
    ProductWithDB1.product_type_options.should ==
      [['その他', '09'], ['DVD', '02'], ['CD', '03'], ['本', '01']]
    
    # DBからエントリを動的に追加することも可能です。
    item_toys = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 5, :item_cd => '04', :name => 'おもちゃ')
    ProductWithDB1.product_type_options.should == 
      [['その他', '09'], ['DVD', '02'], ['CD', '03'], ['本', '01'], ['おもちゃ', '04']]
    ProductWithDB1.product_type_key_by_id('04').should == :entry_04
    
    # DBからレコードを削除してもコードで定義したentryは削除されません。
    # 順番はDBからの取得順で並び替えられたものの後になります
    item_dvd.destroy
    ProductWithDB1.product_type_options.should == 
      [['その他', '09'], ['CD', '03'], ['本', '01'], ['おもちゃ', '04'], ['DVD', '02']]
    
    # DB上で追加したレコードを削除すると、エントリも削除されます
    item_toys.destroy
    ProductWithDB1.product_type_options.should == 
      [['その他', '09'], ['CD', '03'], ['本', '01'], ['DVD', '02']]
    
    # 名称を指定していたDBのレコードを削除したら元に戻ります。
    item_book.destroy
    ProductWithDB1.product_type_options.should == 
      [['その他', '09'], ['CD', '03'], ['書籍', '01'], ['DVD', '02']]
    
    # エントリに該当するレコードを全部削除したら、元に戻ります。
    ItemMaster.delete_all("category_name = 'product_type_cd'")
    ProductWithDB1.product_type_options.should == 
      [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
    
    assert_product_discount(ProductWithDB1)
  end
  
  

  
  # Q: product_type_cd の'_cd'はどこにいっちゃったの?
  # A: デフォルトでは、/(_cd$|_code$|_cds$|_codes$)/ を削除したものをbase_nameとして
  #    扱い、それに_keyなどを付加してメソッド名を定義します。もしこのルールを変更したい場合、
  #    selectable_attrを使う前に selectable_attr_name_pattern で新たなルールを指定してください。
  class Product2 < ActiveRecord::Base
    set_table_name 'products'
    self.selectable_attr_name_pattern = /^product_|_cd$/
    
    selectable_attr :product_type_cd do
      entry '01', :book, '書籍', :discount => 0.8
      entry '02', :dvd, 'DVD', :discount => 0.2
      entry '03', :cd, 'CD', :discount => 0.5
      entry '09', :other, 'その他', :discount => 1
    end

    def discount_price
      (type_entry[:discount] * price).to_i
    end
  end
  
  it "test_product2" do
    assert_product_discount(Product2)
    # 選択肢を表示するためのデータは以下のように取得できる
    Product2.type_options.should ==
      [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]

    p2 = Product2.new
    p2.product_type_cd.should be_nil
    p2.type_key.should be_nil
    p2.type_name.should be_nil
    # idを変更すると得られるキーも名称も変わります
    p2.product_type_cd = '02'
    p2.product_type_cd.should == '02'
    p2.type_key.should == :dvd
    p2.type_name.should == 'DVD'
    # キーを変更すると得られるidも名称も変わります
    p2.type_key = :book
    p2.product_type_cd.should == '01'
    p2.type_key.should == :book 
    p2.type_name.should == '書籍'
    # id、キー、名称以外の任意の属性は、entryの[]メソッドで取得します。
    p2.type_key = :cd
    p2.type_entry[:discount].should == 0.5
    
    Product2.type_id_by_key(:book).should == '01'
    Product2.type_id_by_key(:dvd).should == '02'
    Product2.type_name_by_key(:cd).should == 'CD'
    Product2.type_name_by_key(:other).should == 'その他'
    Product2.type_key_by_id('09').should == :other
    Product2.type_name_by_id('01').should == '書籍'
    Product2.type_keys.should == [:book, :dvd, :cd, :other]
    Product2.type_names.should == ['書籍', 'DVD', 'CD', 'その他']
    Product2.type_keys('02', '03').should == [:dvd, :cd]
    Product2.type_names(:cd, :dvd).should == ['CD', 'DVD']
  end
  
  
  
  
  # Q: selectable_attrの呼び出し毎にbase_bname(って言うの?)を指定したいんだけど。
  # A: base_nameオプションを指定してください。
  class Product3 < ActiveRecord::Base
    set_table_name 'products'
    
    selectable_attr :product_type_cd, :base_name => 'type' do
      entry '01', :book, '書籍', :discount => 0.8
      entry '02', :dvd, 'DVD', :discount => 0.2
      entry '03', :cd, 'CD', :discount => 0.5
      entry '09', :other, 'その他', :discount => 1
    end

    def discount_price
      (type_entry[:discount] * price).to_i
    end
  end
  
  it "test_product3" do 
    assert_product_discount(Product3)
    # 選択肢を表示するためのデータは以下のように取得できる
    Product3.type_options.should ==
      [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
      
    p3 = Product3.new
    p3.product_type_cd.should be_nil
    p3.type_key.should be_nil
    p3.type_name.should be_nil
    # idを変更すると得られるキーも名称も変わります
    p3.product_type_cd = '02'
    p3.product_type_cd.should == '02'
    p3.type_key.should ==        :dvd
    p3.type_name.should ==       'DVD'
    # キーを変更すると得られるidも名称も変わります
    p3.type_key = :book
    p3.product_type_cd.should == '01'
    p3.type_key.should ==        :book
    p3.type_name.should ==       '書籍'
    # id、キー、名称以外の任意の属性は、entryの[]メソッドで取得します。
    p3.type_key = :cd
    p3.type_entry[:discount].should == 0.5
    
    Product3.type_id_by_key(:book).should ==    '01'
    Product3.type_id_by_key(:dvd).should ==     '02'    
    Product3.type_name_by_key(:cd).should ==    'CD'    
    Product3.type_name_by_key(:other).should == 'その他'
    Product3.type_key_by_id('09').should ==     :other  
    Product3.type_name_by_id('01').should ==    '書籍'  
    Product3.type_keys.should == [:book, :dvd, :cd, :other]
    Product3.type_names.should == ['書籍', 'DVD', 'CD', 'その他']
    Product3.type_keys('02', '03').should == [:dvd, :cd]
    Product3.type_names(:cd, :dvd).should == ['CD', 'DVD']
  end
  
end