#
# Copyright (C) 2006 SIPfoundry Inc.
# Licensed by SIPfoundry under the LGPL license.
# 
# Copyright (C) 2006 Pingtel Corp.
# Licensed to SIPfoundry under a Contributor Agreement.
#
##############################################################################

$SOURCE_DIR = File.dirname(__FILE__)    # directory in which this file is located

# system requirements
require 'parsedate'
require File.join($SOURCE_DIR, '..', 'test_helper')

# application requirements
require File.join($SOURCE_DIR, '..', '..', 'call_resolver')


# :TODO: Make it easy to run all the unit tests, possibly via Rakefile, for build loop.
class CallResolverTest < Test::Unit::TestCase
  fixtures :call_state_events, :cdrs
  
  TEST_AOR = 'aor'
  TEST_CONTACT = 'contact'
  TEST_CALL_ID = 'call ID'
  TEST_FROM_TAG = 'f'
  TEST_TO_TAG = 't'
 
  SECONDS_IN_A_DAY = 24 * 60 * 60
  
  CALL_ID1 = 'call_id1'
  CALL_ID2 = 'call_id2'
  CALL_ID3 = 'call_id3'
  
  TEST_DB1 = 'SIPXCSE_TEST1'
  TEST_DB2 = 'SIPXCSE_TEST2'
 
  LOCALHOST = 'localhost'
 
public

  def setup
    super
    
    # Create the CallResolver, giving it the location of the test config file.
    @resolver = CallResolver.new(File.join($SOURCE_DIR, 'data/callresolver-config'))
  end

  def test_connect_to_cdr_database
    # Ensure that we are disconnected first
    if ActiveRecord::Base.connected?
      ActiveRecord::Base.remove_connection
    end
    assert(!ActiveRecord::Base.connected?, 'Must be disconnected from database')
    
    # Connect to the database and verify that we are connected.
    # Rails quirk: we can't assert "connected?" because the connection has been
    # established but is not active yet, so "connected?" is not true.  But we
    # can check that the connection exists.
    @resolver.send(:connect_to_cdr_database)
    assert(ActiveRecord::Base.connection, 'Must be connected to database')
  end

  def test_load_distrib_events_in_time_window
    # Create the two test databases if they don't already exist.
    # Note: using existing databases to save time may fail if the schema changes
    # in, which case you should manually delete the databases and run the test
    # again.
    DatabaseUtils.create_cdr_database(TEST_DB1)
    DatabaseUtils.create_cdr_database(TEST_DB2)
    
    cse_url1 = DatabaseUrl.new(TEST_DB1)
    cse_url2 = DatabaseUrl.new(TEST_DB2)
    
    # Pick an arbitrary base event time for all events
    start_time = Time.now
    
    # Test loading the events.  Restore the CallStateEvent DB connection after
    # the test if there was one.  Restore the CSE database URLs.
    conn = CallStateEvent.remove_connection
    cse_database_urls = @resolver.send(:cse_database_urls)
    begin
      # Put events for the first call and part of the second call into the first
      # test DB.
      # Note: only the time values on the call that is split across two DBs
      # matter, because only that call will be time-sorted when the events get
      # merged.
      CallStateEvent.establish_connection(DatabaseUrl.new(TEST_DB1).to_hash)
      CallStateEvent.destroy_all
      e1_1 = create_test_cse(CALL_ID1, start_time)
      e1_2 = create_test_cse(CALL_ID1, start_time)
      e2_1 = create_test_cse(CALL_ID2, start_time + 1)
      e2_3 = create_test_cse(CALL_ID2, start_time + 3)
      
      # Put events for the rest of the second call and the third call into the
      # second test DB.
      CallStateEvent.establish_connection(DatabaseUrl.new(TEST_DB2).to_hash)
      CallStateEvent.destroy_all
      e2_2 = create_test_cse(CALL_ID2, start_time + 2)
      e2_4 = create_test_cse(CALL_ID2, start_time + 4)
      e3_1 = create_test_cse(CALL_ID3, start_time)
      
      call1 = [e1_1, e1_2]
      call2_part1 = [e2_1, e2_3]
      call2_part2 = [e2_2, e2_4]
      call2 = (call2_part1 + call2_part2).sort!{|x, y| x.event_time <=> y.event_time}
      call3 = [e3_1]
      all_calls = [[call1, call2_part1],           # call arrays for first DB
                   [call2_part2, call3]]           # call arrays for second DB
      
      end_time = start_time + 10
      @resolver.config.send(:cse_database_urls=, [cse_url1, cse_url2])
      call_map = @resolver.send(:load_distrib_events_in_time_window, start_time, end_time)
      expected_call_map_entries = [call1, call2, call3]
      check_call_map(all_calls, call_map, expected_call_map_entries)
    
    ensure
      # Restore the original connection, or at least clear the test connection
      if conn
        CallStateEvent.establish_connection(conn)
      else
        CallStateEvent.remove_connection
      end
      
      # Restore original DB URLs
      @resolver.config.send(:cse_database_urls=, cse_database_urls)
    end
  end

  # Create a test CSE.  Fill in dummy values for fields we don't care about but
  # have to be filled in because of not-null DB constraints
  def create_test_cse(call_id, event_time)
    CallStateEvent.create(:observer => 'observer',
                          :event_seq => 0,
                          :event_time => event_time,
                          :event_type => CallStateEvent::CALL_REQUEST_TYPE,
                          :cseq => 0,
                          :call_id => call_id,
                          :from_url => 'from_url',
                          :to_url => 'to_url')    
  end

  def test_merge_events_for_call    
    e1 = CallStateEvent.new(:call_id => CALL_ID1)
    e2 = CallStateEvent.new(:call_id => CALL_ID1)
    
    time = Time.now;
    e3 = CallStateEvent.new(:call_id => CALL_ID2, :event_time => time)
    e4 = CallStateEvent.new(:call_id => CALL_ID2, :event_time => time + 1)
    e5 = CallStateEvent.new(:call_id => CALL_ID2, :event_time => time + 2)
    e6 = CallStateEvent.new(:call_id => CALL_ID2, :event_time => time + 3)
    
    e7 = CallStateEvent.new(:call_id => CALL_ID3)
    
    call1 = [e1, e2]
    call2_part1 = [e3, e5]
    call2_part2 = [e4, e6]
    call2 = [e3, e4, e5, e6]
    call3 = [e7]
    all_calls = [[call1, call2_part1],           # call arrays for first DB
                 [call2_part2, call3]]           # call arrays for second DB
    call_map = {}
    @resolver.send(:merge_events_for_call, all_calls, call_map)
    expected_call_map_entries = [call1, call2, call3]
    check_call_map(all_calls, call_map, expected_call_map_entries)
  end

  def check_call_map(all_calls, call_map, expected_call_map_entries)
    assert_equal(expected_call_map_entries.size, call_map.size)
    [CALL_ID1, CALL_ID2, CALL_ID3].each_with_index do |call_id, i|
      assert_equal(expected_call_map_entries[i], call_map[call_id])
    end
  end

  def test_split_events_by_call
    call1 = 'call1'
    call2 = 'call2'
    call3 = 'call3'
    e1 = CallStateEvent.new(:call_id => call1)
    e2 = CallStateEvent.new(:call_id => call2)
    e3 = CallStateEvent.new(:call_id => call2)
    e4 = CallStateEvent.new(:call_id => call3)
    e5 = CallStateEvent.new(:call_id => call3)
    e6 = CallStateEvent.new(:call_id => call3)
    events = [e1, e2, e3, e4, e5, e6]
    spl = @resolver.send(:split_events_by_call, events)
    assert_equal([e1], spl[0])
    assert_equal([e2, e3], spl[1])
    assert_equal([e4, e5, e6], spl[2])
    
    # check empty events array as input
    assert_equal([], @resolver.send(:split_events_by_call, []))
  end

  def test_load_call_ids
    start_time = Time.parse('1990-05-17T19:30:00.000Z')
    end_time = Time.parse('1990-05-17T19:45:00.000Z')

    # Load call IDs.  Do a low level message send to bypass access control on 
    # this private method.
    call_ids = @resolver.send(:load_call_ids, start_time, end_time)
    
    # call IDs can come back in any order, so sort them to guarantee order
    call_ids.sort!
    
    # verify results
    assert_equal(3, call_ids.length, 'Wrong number of call IDs')
    assert_equal('testSimpleSuccess',
                 call_ids[0],
                 'Wrong first call ID')
    assert_equal('testSimpleSuccessBogusCallInTimeWindow',
                 call_ids[1],
                 'Wrong second call ID')
  end
  
  def test_load_events_in_time_window
    start_time = Time.parse('1990-05-17T19:30:00.000Z')
    end_time = Time.parse('1990-05-17T19:45:00.000Z')
    events = @resolver.send(:load_events_in_time_window, start_time, end_time)
    assert_equal(6, events.length, 'Wrong number of events')
    assert_equal('testSimpleSuccess', events[0].call_id, 'Wrong first call ID')
    assert_equal('testSimpleSuccess_CalleeEnd',
                 events[5].call_id,
                 'Wrong last call ID')
    assert_equal(1, events[0].cseq, 'Wrong first cseq')
    assert_equal(2, events[1].cseq, 'Wrong second cseq')
    assert_equal(3, events[2].cseq, 'Wrong third cseq')
  end
  
  def test_load_events_with_call_id
    events = load_simple_success_events
    assert_equal(3, events.length)
    events.each_index do |index|
      assert_equal(events[index].event_seq, index + 1, 'Events are not in the right order')
      assert_equal('testSimpleSuccess', events[index].call_id)
    end
  end
  
  def test_find_call_request
    assert_nil(find_call_request([]),
               "No events => must not find call request event")    
    
    # Create call request events.  An original call request has no to_tag.
    orig_req = new_call_request(Time.parse('2001-1-1'))
    req1 = new_call_request(Time.parse('2002-1-1'), 'req1')
    req2 = new_call_request(Time.parse('2003-1-1'), 'req2')
  
    # Create setup and end events  
    setup = CallStateEvent.new(:event_type => CallStateEvent::CALL_SETUP_TYPE)
    call_end = CallStateEvent.new(:event_type => CallStateEvent::CALL_END_TYPE)
  
    assert_nil(find_call_request([setup, call_end]),
               "No call request events => must not find call request event")   
    
    # When there are multiple call requests with no originals, we pick the first
    # one in the array, under the assumption that the caller has already sorted
    # the array by time.
    req = find_call_request([setup, req1, call_end, req2])
    assert_equal(req1, req,
                 'Multiple call requests with no originals => first one must win')
    
    req = find_call_request([req1, orig_req, req2])
    assert_equal(orig_req, req,
                 'Original call requests must be selected before call requests with to_tags')
    
    orig_req2 = new_call_request(Time.parse('2000-1-1'))
    req = find_call_request([call_end, orig_req2, orig_req, setup])
    assert_equal(orig_req2, req,
                 'Multiple original call requests => first one must win')
  end
  
  def new_call_request(event_time, to_tag = nil)
    params = {:event_type => CallStateEvent::CALL_REQUEST_TYPE,
              :event_time => event_time}
    params[:to_tag] = to_tag if to_tag
    CallStateEvent.new(params)
  end
  
  def find_call_request(events)
    @resolver.send(:find_call_request, events)
  end
  
  def test_start_cdr
    cdr = @resolver.send(:start_cdr,
                         call_state_events('testSimpleSuccess_1'))
    # verify the caller
    assert_equal('sip:alice@example.com', cdr.caller_aor)
    assert_equal('sip:alice@1.1.1.1', cdr.caller_contact)
    
    # verify the callee: we have the AOR but not the contact
    assert_equal('sip:bob@example.com', cdr.callee_aor)
    assert_nil(cdr.callee_contact)

    # verify the CDR
    assert_equal('testSimpleSuccess', cdr.call_id)
    assert_equal('f', cdr.from_tag)
    assert_nil(cdr.to_tag)    # don't have the to tag yet
    assert_equal(Time.parse('1990-05-17T19:30:00.000Z'), cdr.start_time)
    assert_equal(Cdr::CALL_REQUESTED_TERM, cdr.termination)
  end

  def test_best_call_leg
    events = load_simple_success_events
     
    # Pick the call leg with the best outcome and longest duration to be the
    # basis for the CDR.
    to_tag = @resolver.send(:best_call_leg, events)
    assert_equal('t', to_tag, 'Wrong to_tag for best call leg')
    
    # load events for the complicated case
    call_id = 'testComplicatedSuccess'
    events = @resolver.send(:load_events_with_call_id, call_id)
     
    to_tag = @resolver.send(:best_call_leg, events)
    assert_equal('t2', to_tag, 'Wrong to_tag for best call leg')
    
    # try again, drop the final call_end event
    to_tag = @resolver.send(:best_call_leg, events[0..4])
    assert_equal('t1', to_tag, 'Wrong to_tag for best call leg')
    
    # try again with three events
    to_tag = @resolver.send(:best_call_leg, events[0..2])
    assert_equal('t0', to_tag, 'Wrong to_tag for best call leg')
    
    # try again with two events
    to_tag = @resolver.send(:best_call_leg, events[0..1])
    assert_equal('t0', to_tag, 'Wrong to_tag for best call leg')
    
    # try again with just the call request
    to_tag = @resolver.send(:best_call_leg, events[0..0])
    assert_nil(to_tag, 'Wrong to_tag for best call leg')
  end

  def test_finish_cdr
    events = load_simple_success_events
    
    # fill in cdr_data with info from the events
    to_tag = 't'
    cdr = Cdr.new
    status = @resolver.send(:finish_cdr, cdr, events, to_tag)
    assert_equal(true, status)
    
    # Check that the CDR is filled in as expected.  It will only be partially
    # filled in because we are testing just one part of the process.
    assert_equal(to_tag, cdr.to_tag, 'Wrong to_tag')
    assert_equal(Time.parse('1990-05-17T19:31:00.000Z'), cdr.connect_time,
                            'Wrong connect_time')
    assert_equal(Time.parse('1990-05-17T19:40:00.000Z'), cdr.end_time,
                            'Wrong end_time')
    assert_equal('sip:bob@2.2.2.2', cdr.callee_contact, 'Wrong callee contact')
    assert_equal(Cdr::CALL_COMPLETED_TERM, cdr.termination, 'Wrong termination code')
    assert_nil(cdr.failure_status)
    assert_nil(cdr.failure_reason)
    
    # Test a failed call.  Check only that the failure info has been filled in
    # properly.  We've checked other info in the case above.
    # This set of events has call request, call setup, call failed.
    call_id = 'testFailed'
    events = @resolver.send(:load_events_with_call_id, call_id)
    check_failed_call(events, to_tag)
    
    # Try again without the call setup event.
    events.delete_if {|event| event.call_setup?}
    check_failed_call(events, to_tag)
  end
  
  def test_finish_cdr_callee_hangs_up
    events = load_simple_success_events_callee_hangs_up
    
    # fill in cdr_data with info from the events
    to_tag = 't'
    cdr = Cdr.new
    status = @resolver.send(:finish_cdr, cdr, events, to_tag)
    assert_equal(true, status)
    
    # Check that the CDR is filled in as expected.  It will only be partially
    # filled in because we are testing just one part of the process.
    assert_equal(to_tag, cdr.to_tag, 'Wrong to_tag')
    assert_equal(Time.parse('1990-05-17T19:41:00.000Z'), cdr.connect_time,
                            'Wrong connect_time')
    assert_equal(Time.parse('1990-05-17T19:50:00.000Z'), cdr.end_time,
                            'Wrong end_time')
    assert_equal('sip:bob@2.2.2.2', cdr.callee_contact, 'Wrong callee contact')
    assert_equal(Cdr::CALL_COMPLETED_TERM, cdr.termination, 'Wrong termination code')
    assert_nil(cdr.failure_status)
    assert_nil(cdr.failure_reason)
  end

  # Helper method for test_finish_cdr.  Check that failure info has been filled
  # in properly.
  def check_failed_call(events, to_tag)
    cdr = Cdr.new
    status = @resolver.send(:finish_cdr, cdr, events, to_tag)
    assert_equal(true, status, 'Finishing the CDR failed')
    assert_equal(Cdr::CALL_FAILED_TERM, cdr.termination, 'Wrong termination code')
    assert_equal(499, cdr.failure_status)
    assert_equal("You Can't Always Get What You Want", cdr.failure_reason) 
  end
  
  def test_save_cdr
    # For a clean test, make sure there is no preexisting CDR with the call
    # ID that we are using.
    Cdr.delete_all("call_id = '#{TEST_CALL_ID}'")
    
    # Create a new complete CDR.  Fill in mandatory fields so we don't get
    # database integrity exceptions on save. 
    cdr = Cdr.new(:call_id =>     TEST_CALL_ID,
                  :from_tag =>    TEST_FROM_TAG,
                  :to_tag =>      TEST_TO_TAG,
                  :caller_aor =>  'colbert@example.com',
                  :callee_aor =>  'report@example.com',
                  :termination => Cdr::CALL_REQUESTED_TERM)
    
    # Try to save it and confirm that it was saved.  Use a clone so we don't
    # modify the original and can reuse it.
    saved_cdr = @resolver.send(:save_cdr, cdr.clone)
    assert(saved_cdr.id, 'ID of object saved to database must be non-nil')
    
    # Try to save another clone, marked as complete.  Because the saved CDR
    # was incomplete based on its termination, the save should succeed.
    cdr2 = cdr.clone
    cdr2.termination = Cdr::CALL_COMPLETED_TERM
    save_again_cdr = @resolver.send(:save_cdr, cdr2)
    assert_not_equal(saved_cdr.id, save_again_cdr.id)
    assert_equal(Cdr::CALL_COMPLETED_TERM, save_again_cdr.termination)
    
    # Try to save another clone. Should fail because the saved CDR is
    # incomplete.  Tweak the termination to help verify this.
    cdr3 = cdr.clone
    cdr3.termination = Cdr::CALL_IN_PROGRESS_TERM
    saved_cdr = @resolver.send(:save_cdr, cdr3)
    assert_equal(save_again_cdr.id, saved_cdr.id)
    assert_equal(cdr2.termination, saved_cdr.termination)    
  end
  
  def test_find_cdr
    # Find a CDR we know is in the DB
    cdr_to_find = Cdr.new(:call_id => 'call1')
    cdr = @resolver.send(:find_cdr, cdr_to_find)
    assert(cdr, "Couldn't find CDR")
    
    # Try to find a CDR we know is not in the DB
    cdr_to_find.call_id = 'extra_bogus_call_id'
    cdr = @resolver.send(:find_cdr, cdr_to_find)
    assert_nil(cdr, "Found a CDR that shouldn't exist")
    
    # Trigger an ArgumentError
    cdr_to_find.call_id = nil
    assert_raise(ArgumentError) {cdr = @resolver.send(:find_cdr, cdr_to_find)}
  end
  
  def test_resolve_call
    ['testSimpleSuccess', 'testComplicatedSuccess', 'testFailed'].each do |call_id|
      events = @resolver.send(:load_events_with_call_id, call_id)
      @resolver.send(:resolve_call, events)
      cdr = Cdr.find_by_call_id(call_id)
      assert_not_nil(cdr, 'CDR was not created')
    end
  end
    
  def test_resolve
    Cdr.delete_all
    start_time = Time.parse('1990-01-1T000:00:00.000Z')
    end_time = Time.parse('2000-12-31T00:00.000Z')
    @resolver.resolve(start_time, end_time)
    assert_equal(4, Cdr.count, 'Wrong number of CDRs')
  end
  
  def test_get_start_time_from_cdr
    Cdr.delete_all
    start_time = @resolver.send(:get_daily_start_time)
    # Give it one second difference in case the timing is off a bit
    assert((Time.now() - (start_time+SECONDS_IN_A_DAY)) <= 1)

    start_time = Time.parse('1990-01-1T000:00:00.000Z')
    end_time = Time.parse('2000-12-31T00:00.000Z')
    @resolver.resolve(start_time, end_time)
    assert_equal(4, Cdr.count, 'Wrong number of CDRs')
    
    start_time = @resolver.send(:get_daily_start_time)
    assert_equal(start_time, Time.gm(2000,1,1,0,0,0))      
  end

  #-----------------------------------------------------------------------------
  # Helper methods
  
  # load and return events for the simple case
  def load_simple_success_events
    call_id = 'testSimpleSuccess'
    @resolver.send(:load_events_with_call_id, call_id)
  end
 
  # load and return events for the simple case
  def load_simple_success_events_callee_hangs_up
    call_id = 'testSimpleSuccess_CalleeEnd'
    @resolver.send(:load_events_with_call_id, call_id)
  end  
  
end


syntax highlighted by Code2HTML, v. 0.9.1