# -*- coding: utf-8 -*-
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

require 'amqp'
require 'time'

describe "Tengine::Event" do
  expected_source_name = "this_server1/12345"

  before do
    Tengine::Event.stub!(:host_name).and_return("this_server1")
    Process.stub!(:pid).and_return(12345)
  end

  # hack!
  after :each do
    EM.instance_eval do
      @next_tick_mutex.synchronize do
        @next_tick_queue = nil
      end
    end
  end

  describe :new_object do
    context "without config" do
      before do
        Tengine::Event.config = {}
        @now = Time.now
        Time.stub!(:now).and_return(@now)
      end

      it{ Tengine::Event.default_source_name.should == expected_source_name }
      it{ Tengine::Event.default_sender_name.should == expected_source_name }
      it{ Tengine::Event.default_level.should == 2 }

      subject{ Tengine::Event.new }
      it{ subject.should be_a(Tengine::Event) }
      its(:key){ should =~ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ }
      its(:event_type_name){ should be_nil }
      its(:source_name){ should == expected_source_name}
      its(:occurred_at){ should == @now.utc }
      its(:level){ should == 2}
      its(:level_key){ should == :info}
      its(:sender_name){ should == expected_source_name }
      its(:properties){ should be_a(Hash) }
      its(:properties){ should be_empty }
      it {
        attrs = subject.attributes
        attrs.should be_a(Hash)
        attrs.delete(:key).should_not be_nil
        attrs.should == {
          :occurred_at => @now.utc,
          :level=>2,
          :source_name => expected_source_name,
          :sender_name => expected_source_name,
        }
      }
      it {
        hash = JSON.parse(subject.to_json)
        hash.should be_a(Hash)
        hash.delete('key').should =~ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
        hash.should == {
          'occurred_at' => @now.utc.iso8601,
          "level" => 2,
          'source_name' => expected_source_name,
          'sender_name' => expected_source_name,
        }
      }
    end

    context "with config" do
      before do
        Tengine::Event.config = {
          :default_source_name => "event_source1",
          :default_sender_name => "sender1",
          :default_level_key => :warn
        }
        @now = Time.now
        Time.stub!(:now).and_return(@now)
      end
      after do
        Tengine::Event.config = {}
      end

      it{ Tengine::Event.default_source_name.should == "event_source1" }
      it{ Tengine::Event.default_sender_name.should == "sender1" }
      it{ Tengine::Event.default_level.should == 3 }

      subject{ Tengine::Event.new }
      it{ subject.should be_a(Tengine::Event) }
      its(:key){ should =~ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ }
      its(:event_type_name){ should be_nil }
      its(:source_name){ should == "event_source1"}
      its(:occurred_at){ should == @now.utc }
      its(:level){ should == 3}
      its(:level_key){ should == :warn}
      its(:sender_name){ should == "sender1" }
      its(:properties){ should be_a(Hash) }
      its(:properties){ should be_empty }
      it {
        attrs = subject.attributes
        attrs.should be_a(Hash)
        attrs.delete(:key).should_not be_nil
        attrs.should == {
          :occurred_at => @now.utc,
          :level=>3,
          :source_name => "event_source1",
          :sender_name => "sender1",
        }
      }
      it {
        hash = JSON.parse(subject.to_json)
        hash.should be_a(Hash)
        hash.delete('key').should =~ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
        hash.should == {
          'occurred_at' => @now.utc.iso8601,
          'level'=>3,
          'source_name' => "event_source1",
          'sender_name' => "sender1",
        }
      }
    end

  end

  describe :new_object_with_attrs do
    subject{ Tengine::Event.new(
        :event_type_name => :foo,
        :key => "hoge",
        'source_name' => "server1",
        :occurred_at => Time.utc(2011,8,11,12,0),
        :level_key => 'error',
        'sender_name' => "server2",
        :properties => {:bar => "ABC", :baz => 999}
        )}
    it{ subject.should be_a(Tengine::Event) }
    its(:key){ should == "hoge" }
    its(:event_type_name){ should == "foo" }
    its(:source_name){ should == "server1" }
    its(:occurred_at){ should == Time.utc(2011,8,11,12,0) }
    its(:level){ should == 4}
    its(:level_key){ should == :error}
    its(:sender_name){ should == "server2" }
    its(:properties){ should == {'bar' => "ABC", 'baz' => 999}}
    it {
      attrs = subject.attributes
      attrs.should == {
        :event_type_name => 'foo',
        :key => "hoge",
        :source_name => "server1",
        :occurred_at => Time.utc(2011,8,11,12,0),
        :level => 4,
        :sender_name => "server2",
        :properties => {'bar' => "ABC", 'baz' => 999}
      }
    }
    it {
      hash = JSON.parse(subject.to_json)
      hash.should be_a(Hash)
      hash.should == {
        "level"=>4,
        'event_type_name' => 'foo',
        'key' => "hoge",
        'source_name' => "server1",
        'occurred_at' => "2011-08-11T12:00:00Z", # Timeオブジェクトは文字列に変換されます
        'sender_name' => "server2",
        'properties' => {'bar' => "ABC", 'baz' => 999}
      }
    }
  end

  describe 'occurred_at with local_time should convert to UTC' do
    subject{ Tengine::Event.new(
        :occurred_at => Time.parse("2011-08-31 12:00:00 +0900"),
        :key => 'hoge'
        )}
    it{ subject.should be_a(Tengine::Event) }
    its(:occurred_at){ should == Time.utc(2011,8,31,3,0) }
    its(:occurred_at){ should be_utc }
    it {
      hash = JSON.parse(subject.to_json)
      hash.should be_a(Hash)
      hash.should == {
        "level"=>2,
        'key' => "hoge",
        'occurred_at' => "2011-08-31T03:00:00Z", # Timeオブジェクトは文字列に変換されます
        'source_name' => "this_server1/12345",
        'sender_name' => "this_server1/12345",
      }
    }
  end

  it 'attrs_for_new must be Hash' do
    expect{
      Tengine::Event.new("{foo: 1, bar: 2}")
    }.to raise_error(ArgumentError, /attrs must be a Hash but was/)
  end


  describe :occurred_at do
    context "valid nil" do
      subject{ Tengine::Event.new(:occurred_at => nil) }
      it do
        subject.occurred_at = nil
        subject.occurred_at.should == nil
      end
    end
    context "valid String" do
      subject{ Tengine::Event.new(:occurred_at => "2011-08-31 12:00:00 +0900") }
      its(:occurred_at){ should be_a(Time); should == Time.utc(2011, 8, 31, 3)}
    end
    it "invalid Class: Range" do
      expect{
        Tengine::Event.new(:occurred_at => (1..3))
      }.to raise_error(ArgumentError)
    end
  end
  it 'occurred_at must be Time' do
    expect{
      Tengine::Event.new(:occurred_at => "invalid time string")
    }.to raise_error(ArgumentError, /no time information/)
  end

  describe :level do
    {
      0 => :gr_heartbeat,
      1 => :debug,
      2 => :info,
      3 => :warn,
      4 => :error,
      5 => :fatal,
    }.each do |level, level_key|
      context "set by level" do
        subject{ Tengine::Event.new(:level => level) }
        its(:level_key){ should == level_key}
      end
      context "set Symbol by level_key" do
        subject{ Tengine::Event.new(:level_key => level_key.to_sym) }
        its(:level){ should == level}
        its(:level_key){ should == level_key.to_sym}
      end
      context "set String by level_key" do
        subject{ Tengine::Event.new(:level_key => level_key.to_s) }
        its(:level){ should == level}
        its(:level_key){ should == level_key.to_sym}
      end
    end
  end

  describe :properties do
    subject{ Tengine::Event.new }
    it "valid usage" do
      subject.properties.should == {}
      subject.properties = {:foo => 123}
      subject.properties.should == {'foo' => 123}
      subject.properties = {:bar => 456}
      subject.properties.should == {'bar' => 456}
    end
    it "assign nil" do
      subject.properties = nil
      subject.properties.should == {}
    end
  end


  describe :fire do
    before do
      Tengine::Event.config = {
        :connection => {"foo" => "aaa"},
        :exchange => {'name' => "exchange1", 'type' => 'direct', 'durable' => true, 'publish' => {'persistent' => true}},
        :queue => {'name' => "queue1", 'durable' => true},
      }
      @mock_connection = mock(:connection)
      @mock_channel = mock(:channel)
      @mock_exchange = mock(:exchange)
      AMQP.should_receive(:connect).with(an_instance_of(Hash)).and_yield(@mock_connection).and_return(@mock_connection)
      @mock_connection.stub(:connected?).and_return(true)
      @mock_connection.stub(:disconnect).and_yield
      @mock_connection.stub(:server_capabilities).and_return(nil)
      AMQP::Channel.should_receive(:new).with(@mock_connection, an_instance_of(Fixnum), an_instance_of(Hash)).and_yield(@mock_channel).and_return(@mock_channel)
      @mock_channel.stub(:publisher_index).and_return(nil)
      AMQP::Exchange.should_receive(:new).with(@mock_channel, :direct, "exchange1",
        :passive=>false, :durable=>true, :auto_delete=>false, :internal=>false, :nowait=>false).and_yield(@mock_exchange).and_return(@mock_exchange)
      @mock_channel.stub(:close)
    end

    it "JSON形式にserializeしてexchangeにpublishする" do
      expected_event = Tengine::Event.new(:event_type_name => :foo, :key => "uniq_key")
      @mock_exchange.should_receive(:publish).with(expected_event.to_json, :persistent => true, :content_type => "application/json")
      EM.run do
        Tengine::Event.fire(:foo, :key => "uniq_key")
        EM.add_timer(0.1) do
          EM.next_tick do
            EM.stop
          end
        end
      end
    end
  end


  describe :parse do
    context "can parse valid json object" do
      subject do
        source = Tengine::Event.new(
          :event_type_name => :foo,
          :key => "hoge",
          'source_name' => "server1",
          :occurred_at => Time.utc(2011,8,11,12,0),
          :level_key => 'error',
          'sender_name' => "server2",
          :properties => {:bar => "ABC", :baz => 999}
          )
        Tengine::Event.parse(source.to_json)
      end
      its(:key){ should == "hoge" }
      its(:event_type_name){ should == "foo" }
      its(:source_name){ should == "server1" }
      its(:occurred_at){ should == Time.utc(2011,8,11,12,0) }
      its(:level){ should == 4}
      its(:level_key){ should == :error}
      its(:sender_name){ should == "server2" }
      its(:properties){ should == {'bar' => "ABC", 'baz' => 999}}
    end

    context "can parse valid json array" do
      before do
        source1 = Tengine::Event.new(
          :event_type_name => :foo,
          :key => "hoge1",
          'source_name' => "server1",
          :occurred_at => Time.utc(2011,8,11,12,0),
          :level_key => 'error',
          'sender_name' => "server2",
          :properties => {:bar => "ABC", :baz => 999}
          )
        source2 = Tengine::Event.new(
          :event_type_name => :bar,
          :key => "hoge2",
          'source_name' => "server3",
          :occurred_at => Time.utc(2011,9,24,15,50),
          :level_key => 'warn',
          'sender_name' => "server4",
          :properties => {:bar => "DEF", :baz => 777}
          )
        @parsed = Tengine::Event.parse([source1, source2].to_json)
      end

      context "first" do
        subject{ @parsed.first }
        its(:key){ should == "hoge1" }
        its(:event_type_name){ should == "foo" }
        its(:source_name){ should == "server1" }
        its(:occurred_at){ should == Time.utc(2011,8,11,12,0) }
        its(:level){ should == 4}
        its(:level_key){ should == :error}
        its(:sender_name){ should == "server2" }
        its(:properties){ should == {'bar' => "ABC", 'baz' => 999}}
      end

      context "last" do
        subject{ @parsed.last }
        its(:key){ should == "hoge2" }
        its(:event_type_name){ should == "bar" }
        its(:source_name){ should == "server3" }
        its(:occurred_at){ should == Time.utc(2011,9,24,15,50) }
        its(:level){ should == 3}
        its(:level_key){ should == :warn}
        its(:sender_name){ should == "server4" }
        its(:properties){ should == {'bar' => "DEF", 'baz' => 777}}
      end
    end

    it "raise ArgumentError for invalid attribute name" do
      expect{
        Tengine::Event.parse({'name' => :foo}.to_json)
      }.to raise_error(NoMethodError)
    end

  end

  describe "#transmitted?" do

    subject { Tengine::Event.new }

    it "returns false while not transmitted" do
      Tengine::Mq::Suite.stub(:pending?).with(subject).and_return(true)
      subject.transmitted?.should be_false
    end

    it "returns true once transmitted" do
      Tengine::Mq::Suite.stub(:pending?).with(subject).and_return(false)
      subject.transmitted?.should be_true
    end
  end

end