#!/usr/bin/env python
# -*- coding: utf-8 -*-
#   Copyright 2008 Agile42 GmbH - Andrea Tomasini 
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
# 
# Authors:
#     - Andrea Tomasini
#     - Felix Schwarz
import re
import unittest

from agilo.ticket.links import LinkOption
from agilo.ticket.links.model import LinksConfiguration
from agilo.ticket.model import AgiloTicket
from agilo.utils import Key, Type
from agilo.utils.config import AgiloConfig, initialize_config
from agilo.test import TestEnvHelper
from agilo.config import __CONFIG_PROPERTIES__


class TestOperationOnLinksInAgiloTicket(unittest.TestCase):
    
    def _reset_links_configuration(self):
        # Reinitialize the link configuration
        lc = LinksConfiguration(self.teh.env)
        lc._initialized = False
        lc.initialize()
    
    def setUp(self):
        # Creates the environment and a couple of tickets
        self.teh = TestEnvHelper()
        self.env = self.teh.get_env()
        config = AgiloConfig(self.env)
        # Adding properties for multiple calculated property testing
        # The problem is that at this point the linkConfiguration as been
        # already initialized so we will need to do it again manually
        config.change_option('actual_time', 'text', 
                             section=AgiloConfig.TICKET_CUSTOM)
        config.change_option(Type.TASK, 
                             'sprint, remaining_time, actual_time, estimated_time, owner',
                             section=AgiloConfig.AGILO_TYPES)
        config.change_option('story.calculate', 
                             'total_remaining_time=sum:get_outgoing.remaining_time, total_actual_time=sum:get_outgoing.actual_time',
                             section=AgiloConfig.AGILO_LINKS)
        config.save()
        
        self.assertTrue(config.is_agilo_enabled)
        self.assertTrue('actual_time' in config.get_list(Type.TASK, section=AgiloConfig.AGILO_TYPES))
        self.assertTrue('actual_time' in config.get_fields_for_type().get(Type.TASK))
        self.assertEqual(config.get('story.calculate', section=AgiloConfig.AGILO_LINKS), 
                         'total_remaining_time=sum:get_outgoing.remaining_time, total_actual_time=sum:get_outgoing.actual_time')
        self._reset_links_configuration()
        # Creates tickets
        self.t1 = self.teh.create_ticket(Type.USER_STORY)
        self.t2 = self.teh.create_ticket(Type.TASK, 
                                         props={Key.REMAINING_TIME: u'20', 
                                                Key.RESOURCES: u'Tim, Tom'})
        self.t3 = self.teh.create_ticket(Type.TASK, 
                                         props={Key.REMAINING_TIME: u'10', 
                                                Key.RESOURCES: u'Tim, Tom'})
        # Now actual_time should be a valid field for Task...
        self.assertNotEqual(None, self.t2.get_field('actual_time'))
        
    def tearDown(self):
        """docstring for tearDown"""
        self.teh.delete_created_tickets()
        
    def testCreateAndDeleteLink(self):
        """Creates a link between t1 and t2 and deletes it afterwards"""
        #Creates the link
        self.assertFalse(self.t1.is_linked_to(self.t2))
        self.assertFalse(self.t2.is_linked_from(self.t2))
        self.assertTrue(self.t1.link_to(self.t2))
        self.assertTrue(self.t2.is_linked_from(self.t1))
        self.assertTrue(self.t1.is_linked_to(self.t2))
        #Link again and check if it return false
        self.assertFalse(self.t1.link_to(self.t2))
        self.assertTrue(self.t2.is_linked_from(self.t1))
        self.assertTrue(self.t1.is_linked_to(self.t2))
        # Delete it
        self.assertTrue(self.t1.del_link_to(self.t2))
        # Check if they are still linked
        self.assertFalse(self.t2.is_linked_from(self.t1))
        self.assertFalse(self.t1.is_linked_to(self.t2))
        
    def testCalculatedProperties(self):
        """Test the calculated property for remaining_time on the aggregated tickets"""
        # Creates first a third AgiloTicket
        self.assertTrue(self.t1.link_to(self.t3))
        self.assertTrue(self.t3.is_linked_from(self.t1))
        self.assertTrue(self.t1.is_linked_to(self.t3))
        # Creates the link also with self.t2
        self.assertTrue(self.t1.link_to(self.t2))
        self.assertTrue(self.t2.is_linked_from(self.t1))
        self.assertTrue(self.t1.is_linked_to(self.t2))
        # Now get the remaining_time that is configured as a calculated property
        expected = int(self.t2[Key.REMAINING_TIME]) + int(self.t3[Key.REMAINING_TIME])
        self.assertEqual(expected, self.t1['total_remaining_time'])
        # Change one of the ticket value and ask again for the property
        self.t2[Key.REMAINING_TIME] = u'10'
        self.t2.save_changes('The Tester', 'Changed remaining time to 10...')
        # Now get the remaining_time that is configured as a calculated property
        expected = int(self.t2[Key.REMAINING_TIME]) + int(self.t3[Key.REMAINING_TIME])
        self.assertEqual(expected, self.t1['total_remaining_time'])
    
    def testMultipleCalculatedProperties(self):
        """Tests multiple calculated properties on the same link-type"""
        # Creates first a third AgiloTicket
        self.assertTrue(self.t1.link_to(self.t3))
        self.assertTrue(self.t3.is_linked_from(self.t1))
        self.assertTrue(self.t1.is_linked_to(self.t3))
        # Creates the link also with self.t2
        self.assertTrue(self.t1.link_to(self.t2))
        self.assertTrue(self.t2.is_linked_from(self.t1))
        self.assertTrue(self.t1.is_linked_to(self.t2))
        # Sets the actual_time as a property for t2 and t3
        self.assertTrue('actual_time' in self.t2.fields_for_type)
        self.assertNotEquals(None, self.t2.get_field('actual_time'))
        self.t2['actual_time'] = u'8'
        self.t2.save_changes('The Tester', 'Changed actual time to 8...')
        self.t3['actual_time'] = u'4'
        self.t3.save_changes('The Tester', 'Changed actual time to 4...')
        self.assertEqual(self.t1['total_remaining_time'], 
                         int(self.t2[Key.REMAINING_TIME]) + int(self.t3[Key.REMAINING_TIME]))
        expected_actual_time = int(self.t2['actual_time']) + int(self.t3['actual_time'])
        self.assertEqual(expected_actual_time, self.t1['total_actual_time'])
    
    def testLinkOnDeletedTicket(self):
        """Tests the deletion of a link after a deleted ticket"""
        # Creates the link also with self.t2
        self.assertTrue(self.t1.link_to(self.t2))
        self.assertTrue(self.t2.is_linked_from(self.t1))
        self.assertTrue(self.t1.is_linked_to(self.t2))
        # Now delete the ticket and check propagation
        self.t1.delete()
        self.assertFalse(self.t2.is_linked_from(self.t1))
        
    def testIsLinked(self):
        """Tests the linking control, via all possible methods"""
        self.assertFalse(self.t1.is_linked_to(self.t2))
        self.assertFalse(self.t2.is_linked_from(self.t1))
        # Create the link
        self.assertTrue(self.t1.link_to(self.t2))
        self.assertTrue(self.t1.is_linked_to(self.t2))
        self.assertTrue(self.t2.is_linked_from(self.t1))
        # Delete link and test if is not linked anymore
        self.assertTrue(self.t1.del_link_to(self.t2))
        self.assertFalse(self.t1.is_linked_to(self.t2))
        self.assertFalse(self.t2.is_linked_from(self.t1))
        
    def testBuildDict(self):
        """Test the build dictionary with the list of links"""
        self.assertEqual(len(self.t1.get_outgoing()), len(self.t1.get_outgoing_dict()))
        self.assertEqual(len(self.t2.get_outgoing()), len(self.t2.get_outgoing_dict()))
        self.assertEqual(len(self.t1.get_incoming()), len(self.t1.get_incoming_dict()))
        self.assertEqual(len(self.t2.get_incoming()), len(self.t2.get_incoming_dict()))
        
    def testCreateNewLinkedTicket(self):
        """Test the creation of a new linked ticket"""
        # Get db and handle the transaction
        db = self.teh.get_env().get_db_cnx()
        new = AgiloTicket(self.teh.get_env())
        new[Key.SUMMARY] = 'This is a new ticket, never saved'
        new[Key.DESCRIPTION] = 'This will be created and linked in one shot ;-)'
        new[Key.TYPE] = Type.TASK
        self.assertTrue(new.link_from(self.t1, db))
        self.assertTrue(new.insert(db=db))
        # Now commit the link and the insert
        db.commit()
        # Check that has been linked
        self.assertTrue(new.is_linked_from(self.t1))
        self.assertTrue(self.t1.is_linked_to(new))
        
        # Now test the autoinsert and link of the agilo ticket
        new2 = AgiloTicket(self.teh.get_env(), t_type=Type.TASK)
        new2[Key.SUMMARY] = "This is a linked new ticket"
        new2[Key.DESCRIPTION] = "description"
        self.assertTrue(self.t1.link_to(new2))
        self.assertTrue(new2.is_linked_from(self.t1))
        self.assertTrue(self.t1.is_linked_to(new2))
        
        # Now test the link failure
        self.assertFalse(new2.link_from(self.t1))
    
    def _build_requirement(self, business_value=None):
        props=None
        if business_value is not None:
            props = {Key.BUSINESS_VALUE: str(business_value)}
        requirement = self.teh.create_ticket(Type.REQUIREMENT, props=props)
        return requirement
    
    def _build_requirement_with_two_stories(self, business_value=None, 
                                        points_story1=None, points_story2=None):
        requirement = self._build_requirement(business_value)
        props = {Key.STORY_PRIORITY: 'Mandatory'} 
        if points_story1 is not None:
            props[Key.STORY_POINTS] = str(points_story1)
        story1 = self.teh.create_ticket(Type.USER_STORY, props=props)
        if points_story2 is not None:
            props[Key.STORY_POINTS] = str(points_story2)
        story2 = self.teh.create_ticket(Type.USER_STORY, props=props)
        self.assertTrue(requirement.link_to(story1))
        self.assertTrue(requirement.link_to(story2))
        return requirement, story1, story2
    
    def test_calculation_of_total_story_points(self):
        requirement = self._build_requirement_with_two_stories(500, 16, 4)[0]
        self.assertEqual(20, requirement["total_story_points"])
        self.assertEqual(500, int(requirement[Key.BUSINESS_VALUE]))
        self.assertTrue(requirement.exists)
        self.assertNotEqual(0, requirement.id)
        all_calculated = LinksConfiguration(self.teh.get_env()).get_calculated()
        self.assertTrue('total_story_points' in all_calculated)
        self.assertTrue('total_remaining_time' in all_calculated)
        self.assertTrue('total_actual_time' in all_calculated)
        self.assertTrue(Key.ROIF in all_calculated)
        self.assertTrue(Key.ROIF in requirement.get_calculated_fields_names())
        self.assertEqual(500, int(requirement[Key.BUSINESS_VALUE]))
        self.assertEqual(25, int(requirement[Key.ROIF]))
    
    def test_total_story_point_calculation_without_stories(self):
        requirement = self._build_requirement_with_two_stories(500)[0]
        self.assertTrue("total_story_points" in requirement.get_calculated_fields_names())
        self.assertEqual(None, requirement['total_story_points'])
    
    def test_calculation_of_story_points_as_float(self):
        requirement = self._build_requirement_with_two_stories(500, 2.5, 2.5)[0]
        self.assertEqual(5, requirement["total_story_points"])
    
    def test_roif_calculation_without_stories(self):
        requirement = self._build_requirement(500)
        self.assertTrue(Key.ROIF in requirement.get_calculated_fields_names())
        self.assertEqual(None, requirement[Key.ROIF])
    
    def test_roif_calculation_with_zero_story_points(self):
        requirement = self._build_requirement_with_two_stories(500, 0, 0)[0]
        self.assertTrue(Key.ROIF in requirement.get_calculated_fields_names())
        self.assertEqual(None, requirement[Key.ROIF])
    
    def test_roif_calculation_only_mandatory_stories(self):
        requirement, story1, story2 = self._build_requirement_with_two_stories(500, 40, 10)
        story1[Key.STORY_PRIORITY] = 'Mandatory'
        story2[Key.STORY_PRIORITY] = 'Exciter'
        self.assertTrue(Key.ROIF in requirement.get_calculated_fields_names())
        self.assertEqual(float("12.5"), requirement[Key.ROIF])
    
    def _change_roif_definition(self, acceptable_user_story_priorities):
        env = self.teh.get_env()
        config = env.config
        config_section = 'agilo-links'
        config_key = '%s.%s' % (Type.REQUIREMENT, LinkOption.CALCULATE)
        calc_option = config.get(config_section, config_key)
        calc_option_without_roif_definition = re.sub(r'\s*,?\s*roif=[^,]+(,|$)', '\1', calc_option)
        
        conditionstring = ":".join(acceptable_user_story_priorities)
        new_sum_definition = 'new_roif_sum=sum:get_outgoing.%s|%s=%s' % (Key.STORY_POINTS, Key.STORY_PRIORITY, conditionstring)
        roif_definition = 'roif=div:%s;new_roif_sum' % Key.BUSINESS_VALUE
        new_calc_option = '%s,%s,%s' % (new_sum_definition, roif_definition, calc_option_without_roif_definition)
        
        config.set(config_section, config_key, new_calc_option)
        config.save()
        self._reset_links_configuration()
    
    def test_roif_calculation_no_exciter_stories(self):
        self._change_roif_definition(['Mandatory', 'Linear'])
        requirement, story1, story2 = self._build_requirement_with_two_stories(500, 40, 10)
        story1[Key.STORY_PRIORITY] = 'Exciter'
        story2[Key.STORY_PRIORITY] = 'Linear'
        self.assertTrue(Key.ROIF in requirement.get_calculated_fields_names())
        self.assertEqual(50, requirement[Key.ROIF])
    
    def test_roif_calculation_all_stories(self):
        self._change_roif_definition(['Mandatory', 'Linear', 'Exciter'])
        requirement, story1, story2 = self._build_requirement_with_two_stories(500, 40, 10)
        story1[Key.STORY_PRIORITY] = 'Mandatory'
        story2[Key.STORY_PRIORITY] = 'Exciter'
        self.assertTrue(Key.ROIF in requirement.get_calculated_fields_names())
        self.assertEqual(10, requirement[Key.ROIF])
    
    def test_roif_column_label_for_backlogs(self):
        self._change_roif_definition(['Mandatory', 'Linear', 'Exciter'])
        env = self.teh.get_env()
        agilo_config = AgiloConfig(env)
        all_backlogs = agilo_config.backlog_columns
        product_backlog = all_backlogs[Key.PRODUCT_BACKLOG]
        found = False
        for field in product_backlog:
            if field.get('name') == Key.ROIF:
                self.assertEqual(True, field['show'])
                self.assertEqual('Roif', field['label'])
                found = True
                break
        assert found
    
    def test_missing_agilo_links_allow(self):
        """Tests robustness of config in case the 'allow' parameter is missing"""
        env = self.teh.get_env()
        env.config.remove('agilo-links', 'allow')
        lc = LinksConfiguration(env)
        # Force initialization
        try:
            lc._initialized = False
            lc.initialize()
        except Exception, e:
            self.fail("Error: %s" % str(e))
            
        # Set it back in place
        initialize_config(env, __CONFIG_PROPERTIES__)


if __name__ == '__main__':
    #unittest.main()
    suite = unittest.TestLoader().loadTestsFromTestCase(TestOperationOnLinksInAgiloTicket)
    #suite = unittest.TestSuite()
    #suite.addTest(TestAgiloTicket('testLinkOnDeletedTicket'))
    unittest.TextTestRunner(verbosity=0).run(suite)
