# -*- encoding: utf-8 -*-
#   Copyright 2008 Agile42 GmbH, Berlin (Germany)
#   Copyright 2007 Andrea Tomasini <andrea.tomasini_at_agile42.com>
#
#   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.
#   
#   Author: 
#       - Andrea Tomasini <andrea.tomasini_at_agile42.com>


from datetime import timedelta
import unittest

from trac.util.datefmt import to_datetime
from trac.ticket.model import Milestone

from agilo.scrum import SprintController
from agilo.scrum.backlog import BacklogModelManager
from agilo.scrum.metrics.model import TeamMetrics
from agilo.utils import Type, Key, BacklogType, Status
from agilo.utils.config import AgiloConfig
from agilo.utils.sorting import SortOrder
from agilo.test import TestEnvHelper


def print_backlog(backlog):
    """Print the backlog with properties for debugging"""
    for bi in backlog:
        print ">>> #%d(%s), bv: %s, sp: %s, rt: %s" % \
               (bi.id, bi.ticket.get_type(),
                bi[Key.BUSINESS_VALUE],
                bi[Key.STORY_PRIORITY],
                bi[Key.REMAINING_TIME]) 
        

class TestBacklog(unittest.TestCase):
    """Tests for the Backlog class"""
    
    def setUp(self):
        self.teh = TestEnvHelper()
        self.env = self.teh.get_env()
        self.bmm = BacklogModelManager(self.env)
    
    def _create_sprint_backlog(self):
        """Creates stories and tasks for a Sprint Backlog and returns the Backlog"""
        sprint = self.teh.create_sprint("Test")
        s1 = self.teh.create_ticket(Type.USER_STORY, 
                                    props={Key.STORY_POINTS: '3', 
                                           Key.SPRINT: sprint.name})
        self.assertTrue(s1.link_to(self.teh.create_ticket(Type.TASK, 
                                                          props={Key.REMAINING_TIME: '4',
                                                                 Key.SPRINT: sprint.name})))
        self.assertTrue(s1.link_to(self.teh.create_ticket(Type.TASK,
                                                          props={Key.REMAINING_TIME: '8',
                                                                 Key.SPRINT: sprint.name})))
        self.assertTrue(s1.link_to(self.teh.create_ticket(Type.TASK, 
                                                          props={Key.REMAINING_TIME: '4'})))
        s2 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '5', 
                                                            Key.SPRINT: sprint.name})
        self.assertTrue(s2.link_to(self.teh.create_ticket(Type.TASK, 
                                                          props={Key.REMAINING_TIME: '2',
                                                                 Key.SPRINT: sprint.name})))
        self.assertTrue(s2.link_to(self.teh.create_ticket(Type.TASK, 
                                                          props={Key.REMAINING_TIME: '3'})))
        sprint_backlog = self.bmm.get(name="Sprint Backlog", scope=sprint.name)
        self.assertTrue(sprint_backlog.has_ticket(s1))
        for t1 in s1.get_outgoing():
            self.assertTrue(sprint_backlog.has_ticket(t1), "%s not in backlog..." % t1)
        self.assertTrue(sprint_backlog.has_ticket(s2))
        for t2 in s2.get_outgoing():
            self.assertTrue(sprint_backlog.has_ticket(t2), "%s not in backlog..." % t2)
        self.assertEqual(sprint_backlog.count(), 7)
        return sprint_backlog
    
    def _create_product_backlog(self):
        """Creates Requirements and Stories for a Product Backlog and returns the Backlog"""
        def _create_story(props):
            """Creates a ticket of type story and returns it"""
            return self.teh.create_ticket(Type.USER_STORY, props=props)
        
        r1 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '3000'})
        self.assertTrue(r1.link_to(_create_story({Key.STORY_PRIORITY: 'Linear'})))
        self.assertTrue(r1.link_to(_create_story({Key.STORY_PRIORITY: 'Exciter'})))
        self.assertTrue(r1.link_to(_create_story({Key.STORY_PRIORITY: 'Mandatory'})))
        r2 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '1200'})
        self.assertTrue(r2.link_to(_create_story({Key.STORY_PRIORITY: 'Mandatory'})))
        self.assertTrue(r2.link_to(_create_story({Key.STORY_PRIORITY: 'Exciter'})))
        r3 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '2000'})
        self.assertTrue(r3.link_to(_create_story({Key.STORY_PRIORITY: 'Mandatory'})))
        r4 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '800'})
        self.assertTrue(r4.link_to(_create_story({Key.STORY_PRIORITY: 'Linear'})))
        r5 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '3000'})
        self.assertTrue(r5.link_to(_create_story({Key.STORY_PRIORITY: 'Exciter'})))
        self.assertTrue(r5.link_to(_create_story({Key.STORY_PRIORITY: 'Mandatory'})))
        product_backlog = self.bmm.get(name="Product Backlog", reload=True)
        self.assertEqual(product_backlog.count(), 14)
        return product_backlog
    
    def testBacklogAsDictDecorator(self):
        """Tests that the Backlog car return itself with the Dict
        Decorator"""
    
    def testBacklogCreation(self):
        """Tests the creation of a Backlog"""
        backlog = self.bmm.create(name="Global Backlog", 
                                  ticket_types=[Type.REQUIREMENT], 
                                  sorting_keys=[(Key.BUSINESS_VALUE, 
                                                 SortOrder.DESCENDING)])
        # Now reload the same backlog and check that the type and order are kept
        b1 = self.bmm.get(name="Global Backlog")
        self.assertEqual(b1.ticket_types, [Type.REQUIREMENT])
        self.assertEqual(b1.sorting_keys, [(Key.BUSINESS_VALUE, SortOrder.DESCENDING)])
        
    def testProductBacklogItems(self):
        """Tests the creation of a Global Backlog and add some Items to it"""
        backlog = self.bmm.create(name="Global Backlog", 
                                  ticket_types=[Type.REQUIREMENT,], 
                                  sorting_keys=[(Key.BUSINESS_VALUE, 
                                                 SortOrder.DESCENDING),])
        # Create some tickets and add them to the Backlog
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '3000'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '1200'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '2000'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '800'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '3000'}))
        # Now test that the ticket are sorted by the defined Key
        ref = 3000
        self.assertTrue(backlog.count() > 0, 'There is no ticket associated to this Backlog!')
        self.assertEqual(5, backlog.count(), 'Wrong number of tickets in this (%s) Backlog!' % \
                                              backlog.count())
        # Sort the Backlog
        backlog.sort()
        # Note that the get_tickets returns BacklogItem objects :-)
        for b in backlog:
            value = int(b[Key.BUSINESS_VALUE])
            self.assertTrue(value <= ref, "%s ! <= %s" % (value, ref))
            ref = value
        # Now save the backlog, load it again, and verify that the order is still ok
        backlog.save()
        b1 = self.bmm.get(name="Global Backlog")
        # Now test that the ticket are sorted by the defined Key
        ref = 3000
        self.assertTrue(b1.count() > 0, 'There is no ticket associated to this Backlog!')
        for b in b1:
            value = int(b.ticket[Key.BUSINESS_VALUE])
            self.assertTrue(value <= ref, "%s ! <= %s" % (value, ref))
            ref = value
        
    def testMoveItemInBacklog(self):
        """Test the movement of an item into the backlog, change the position"""
        backlog = self.bmm.create(name="Global Backlog", 
                                  ticket_types=[Type.REQUIREMENT,], 
                                  sorting_keys=[(Key.BUSINESS_VALUE, 
                                                 SortOrder.DESCENDING),])
        # Create some tickets and add them to the Backlog
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '3000'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '1200'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '2000'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '800'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '3000'}))
        backlog.sort()
        backlog.save()
        b1 = self.bmm.get(name="Global Backlog")
        t2000 = None
        for b in b1:
            #print "b: %s" % type(b)
            if int(b.ticket[Key.BUSINESS_VALUE]) == 2000:
                t2000 = b
                break
        self.assertTrue(t2000 is not None)
        t_pos = t2000.pos
        self.assertTrue(b1.move(t2000, to_pos=t_pos - 2))
        self.assertEqual(t2000.pos, t_pos - 2)
        # Move with the from position too
        self.assertTrue(b1.move(t2000, from_pos=t2000.pos, to_pos=t_pos)) # Move it back
        self.assertEqual(t2000.pos, t_pos)
        # Now try to move a normal ticket and not a BacklogItem
        t2000 = self.teh.load_ticket(t_id=t2000.id)
        self.assertTrue(b1.move(t2000, from_pos=t_pos, to_pos=t_pos - 2))
        
    def testBacklogPositionPersistence(self):
        """Tests the Backlog position persistence of items, after being saved and reloaded"""
        backlog = self.bmm.create(name="Global Backlog", 
                                  ticket_types=[Type.REQUIREMENT,], 
                                  sorting_keys=[(Key.BUSINESS_VALUE, 
                                                 SortOrder.DESCENDING),])
        # Create some tickets and add them to the Backlog
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '3000'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '1200'}))
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '2000'}))
        backlog.save()
        # Now reload the backlog, move the item in pos 2 to 1, save and reload
        b1 = self.bmm.get(name="Global Backlog")
        self.assertEqual(b1.count(), 3)
        t1200 = b1.get_item_in_pos(1)
        self.assertTrue(b1.move(t1200, to_pos=1))
        b1.save()
        # Now reload once more
        b2 = self.bmm.get(name="Global Backlog")
        self.assertEqual(t1200.pos, 1)
        
    def testBacklogWithItemNotAdded(self):
        """Tests the Backlog with Items that have not been explicitly added"""
        backlog = self.bmm.create(name="Global Backlog", 
                                  ticket_types=[Type.REQUIREMENT,], 
                                  sorting_keys=[(Key.BUSINESS_VALUE, 
                                                 SortOrder.DESCENDING),])
        # Create some tickets and add them to the Backlog
        t1 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '3000'})
        t_no = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '13'})
        t2 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '1200'})
        backlog.add(t2)
        backlog.add(self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '2000'}))
        backlog.save()
        # Make sure the t1 is not in the Backlog
        self.assertFalse(backlog.has_ticket(t1))
        # Now reload the Backlog and check if the t1 is loaded too
        b1 = self.bmm.get(name="Global Backlog", 
                          ticket_types=[Type.REQUIREMENT,], 
                          sorting_keys=[(Key.BUSINESS_VALUE, 
                                         SortOrder.DESCENDING),])
        b1.reload()
        # Test that a belonging ticket is really belonging
        self.assertTrue(b1.has_ticket(t2))
        # Test if the external ticket, has been loaded into the Backlog
        self.assertTrue(b1.has_ticket(t1))
        # Test that the t_no, User Story is also not in the Backlog
        self.assertFalse(b1.has_ticket(t_no))

    def testBacklogWithHierarchicalItems(self):
        """Tests the Backlog with multiple hierarchical types"""
        # Create 2 requirements and link 2 stories each
        r1 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '2000'})
        self.assertTrue(r1.link_to(self.teh.create_ticket(Type.USER_STORY, 
                                                          props={Key.STORY_PRIORITY: 'Linear'})))
        self.assertTrue(r1.link_to(self.teh.create_ticket(Type.USER_STORY, 
                                                          props={Key.STORY_PRIORITY: 'Mandatory'})))
        r2 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.BUSINESS_VALUE: '3000'})
        self.assertTrue(r2.link_to(self.teh.create_ticket(Type.USER_STORY, 
                                                          props={Key.STORY_PRIORITY: 'Exciter'})))
        self.assertTrue(r2.link_to(self.teh.create_ticket(Type.USER_STORY, 
                                                          props={Key.STORY_PRIORITY: 'Linear'})))
        # Now create a Backlog with both type and lets' verify that the tickets are there
        backlog = self.bmm.create(name="Global Backlog", 
                                  ticket_types=[Type.REQUIREMENT, Type.USER_STORY], 
                                  sorting_keys=[(Key.BUSINESS_VALUE, SortOrder.DESCENDING),
                                                (Key.STORY_PRIORITY, SortOrder.DESCENDING)])
        self.assertEqual(6, backlog.count())
        self.assertTrue(backlog.has_ticket(r1))
        for at in r1.get_outgoing():
            self.assertTrue(backlog.has_ticket(at))
        self.assertTrue(backlog.has_ticket(r2))
        for at in r2.get_outgoing():
            self.assertTrue(backlog.has_ticket(at))
        # Now test the sorting of the tickets, should respect the Hierarchy
        backlog.sort()
        #print_backlog(backlog)
        self.assertEqual(backlog.get_pos_of_ticket(r2), 0)
        self.assertEqual(backlog.get_pos_of_ticket(r1), 3)
        
    def testSortingWithOneType(self):
        """Test the Backlog sorting with one ticket type"""
        from random import randint
        for i in range(10):
            t = self.teh.create_ticket(Type.REQUIREMENT, 
                                       props={Key.BUSINESS_VALUE: str(randint(100, 3000))})
            self.assertNotEqual(None, t)
            self.assertEqual(Type.REQUIREMENT, t.get_type())
            self.assertEqual(Status.NEW, t[Key.STATUS])
        # Now test that the tickets are there
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        try:
            cursor.execute("SELECT COUNT(*) FROM TICKET WHERE TYPE='%s'" % Type.REQUIREMENT)
            result = cursor.fetchone()
            self.assertEqual(10, result[0])
        except Exception, e:
            self.fail("Error: %s" % str(e))
        
        # Now create a Backlog and sort it
        backlog = self.bmm.create(name="Global Backlog", 
                                  ticket_types=[Type.REQUIREMENT], 
                                  sorting_keys=[(Key.BUSINESS_VALUE, 
                                                 SortOrder.DESCENDING)])
        backlog.sort()
        last = 3000
        self.assertEqual(backlog.count(), 10)
        for i, bi in enumerate(backlog):
            value = int(bi[Key.BUSINESS_VALUE])
            self.assertTrue(last >= value, "bi(%s): %s ! >= %s" % (i, last, value))
            last = value
    
    def testSortingWithMultipleTypes(self):
        """Test the Backlog sorting with more ticket types"""
        t_prop = [(Type.REQUIREMENT, Key.BUSINESS_VALUE),
                  (Type.USER_STORY, Key.STORY_PRIORITY),
                  (Type.TASK, Key.REMAINING_TIME)]
        from random import randint
        # Create some random tickets
        reqs = 2
        stories = 4
        tasks = 3
        bv = 1200
        sp = ['Mandatory', 'Linear', 'Exciter']
        rt = 4
        for i in range(reqs):
            r = self.teh.create_ticket(t_prop[0][0], props={t_prop[0][1]: str(bv)})
            bv = 2000
            for i in range(stories):
                s = self.teh.create_ticket(t_prop[1][0], props={t_prop[1][1]: sp[randint(0,2)]})
                self.assertTrue(r.link_to(s))
                for i in range(tasks):
                    t = self.teh.create_ticket(t_prop[2][0], props={t_prop[2][1]: str(rt)})
                    rt += 2
                    self.assertTrue(s.link_to(t))
            # Creates also a couple of tasks disconnected from other items
            self.teh.create_ticket(t_prop[2][0], props={t_prop[2][1]: str(rt)})
                
                
        # Now create the backlog and sort it
        backlog = self.bmm.create(name="Mega Backlog",
                                  ticket_types=[Type.REQUIREMENT, 
                                                Type.USER_STORY, 
                                                Type.TASK],
                                  sorting_keys=[(Key.BUSINESS_VALUE, 
                                                 SortOrder.DESCENDING),
                                                 (Key.STORY_PRIORITY, 
                                                  SortOrder.DESCENDING),
                                                  (Key.REMAINING_TIME, 
                                                   SortOrder.ASCENDING)])
        backlog.sort()
        rlast = 3000
        slast = ''
        tlast = 100
        last_t = Type.REQUIREMENT
        self.assertEqual(backlog.count(), 
                         (reqs * stories * tasks) +\
                         (reqs * stories) + reqs * 2) # the extra tasks
        for bi in backlog:
            if bi[Key.TYPE] == Type.REQUIREMENT:
                self.assertEqual(0, bi.level)
                bv = int(bi[Key.BUSINESS_VALUE])
                if not bv <= rlast:
                    print_backlog(backlog)
                    self.fail(">>> %s => %s > %s, last_t=%s" % \
                              (bi, bv, rlast, last_t))
                rlast = bv
            elif bi[Key.TYPE] == Type.USER_STORY:
                if 1 != bi.level:
                    print_backlog(backlog)
                    self.fail(">>> Story Level wrong? %s != %s" % (bi.level, 1))
                if last_t != Type.USER_STORY:
                    slast = sp[0] # Reset at every block
                prio = bi[Key.STORY_PRIORITY]
                if not sp.index(prio) >= sp.index(slast):
                    print_backlog(backlog)
                    self.fail(">>> %s => %s < %s, last_t=%s" % \
                              (bi, prio, slast, last_t))
                slast = bi[Key.STORY_PRIORITY]
            else:
                if not bi.level in (0,2):
                    print_backlog(backlog)
                    self.fail("One task is out of place?! %s => level:%s" % \
                              (bi, bi.level))
                if last_t != Type.TASK:
                    tlast = 0 # Reset at every block
                if not int(bi[Key.REMAINING_TIME]) >= tlast:
                    print_backlog(backlog)
                    self.fail(">>> %s => %s > %s, last_t=%s" % \
                              (bi, int(bi[Key.REMAINING_TIME]), tlast, last_t)) # Ascending
                tlast = int(bi[Key.REMAINING_TIME])
            last_t = bi[Key.TYPE]
                
    def testBacklogForSprint(self):
        """Tests a Backlog associated to a Sprint"""
        # Creates a Milestone
        m = Milestone(self.env)
        m.name = "Milestone 1"
        m.insert()
        # Create a Sprint
        sprint = self.teh.create_sprint( 
                        name="Sprint 1", 
                        start=to_datetime(t=None), 
                        duration=20,
                        milestone=m.name)
        # Create some tickets
        # s1(s) -> t1(s)
        #       -> t2(s)
        #       -> t3
        # s2    -> t4(s)
        #       -> t5 
        s1 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '3', 
                                                            Key.SPRINT: sprint.name})
        self.assertEqual(s1[Key.SPRINT], sprint.name)
        self.assertTrue(s1.link_to(self.teh.create_ticket(Type.TASK, props={Key.REMAINING_TIME: '4',
                                                                            Key.SPRINT: sprint.name})))
        self.assertTrue(s1.link_to(self.teh.create_ticket(Type.TASK, props={Key.REMAINING_TIME: '8',
                                                                            Key.SPRINT: sprint.name})))
        self.assertTrue(s1.link_to(self.teh.create_ticket(Type.TASK, props={Key.REMAINING_TIME: '4'})))
        s2 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '5'})
        self.assertTrue(s2.link_to(self.teh.create_ticket(Type.TASK, props={Key.REMAINING_TIME: '2',
                                                                            Key.SPRINT: sprint.name})))
        self.assertTrue(s2.link_to(self.teh.create_ticket(Type.TASK, props={Key.REMAINING_TIME: '3'})))
        # Creates the Backlog bound to the Sprint
        backlog = self.bmm.create(name="Sprint-Backlog", 
                                  ticket_types=[Type.USER_STORY, 
                                                Type.TASK],
                                  sorting_keys=[(Key.STORY_POINTS, 
                                                 SortOrder.DESCENDING),
                                                 (Key.REMAINING_TIME, 
                                                  SortOrder.ASCENDING)],
                                  b_type=BacklogType.SPRINT)
        # The Backlog should contains only the items planned for the Sprint, and with parents
        # planned for the Sprint too
        backlog = self.bmm.get(name="Sprint-Backlog", scope=sprint.name)
        self.assertEqual(backlog.count(), 6)
        # Sort the Backlog
        backlog.sort()
        # Now check the order
        s_last = 100
        t_last = 0
        for bi in backlog:
            if bi[Key.TYPE] == Type.USER_STORY:
                sp = int(bi[Key.STORY_POINTS])
                self.assertTrue(sp <= s_last, "(%s) %s ! <= %s" % (bi, sp, s_last))
                s_last = sp
                t_last = 0 # Is another story
            elif bi[Key.TYPE] == Type.TASK:
                rt = int(bi[Key.REMAINING_TIME])
                self.assertTrue(rt >= t_last, "(%s) %s ! >= %s" % (bi, rt, t_last))
                t_last = rt
        
    def testBacklogForMultipleSprint(self):
        """Tests a Backlog associated to a Sprint with multiple sprints"""
        # Creates a Milestone
        m = Milestone(self.env)
        m.name = "Milestone 1"
        m.insert()
        # Create 2 Sprints
        sprint1 = self.teh.create_sprint(
                         name="Sprint 1", 
                         start=to_datetime(t=None), 
                         duration=20,
                         milestone=m.name)
        sprint2 = self.teh.create_sprint( 
                         name="Sprint 2", 
                         start=to_datetime(t=None), 
                         duration=20,
                         milestone=m.name)
        # Create some tickets
        s1 = self.teh.create_ticket(Type.USER_STORY, 
                                    props={Key.STORY_POINTS: '3', 
                                           Key.SPRINT: sprint1.name})
        self.assertTrue(s1.link_to(self.teh.create_ticket(Type.TASK, 
                                                          props={Key.REMAINING_TIME: '4',
                                                                 Key.SPRINT: sprint1.name})))
        self.assertTrue(s1.link_to(self.teh.create_ticket(Type.TASK,
                                                          props={Key.REMAINING_TIME: '8',
                                                                 Key.SPRINT: sprint1.name})))
        self.assertTrue(s1.link_to(self.teh.create_ticket(Type.TASK, 
                                                          props={Key.REMAINING_TIME: '4'})))
        s2 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '5', 
                                                            Key.SPRINT: sprint2.name})
        self.assertTrue(s2.link_to(self.teh.create_ticket(Type.TASK, 
                                                          props={Key.REMAINING_TIME: '2',
                                                                 Key.SPRINT: sprint2.name})))
        self.assertTrue(s2.link_to(self.teh.create_ticket(Type.TASK, 
                                                          props={Key.REMAINING_TIME: '3'})))
        # Creates the Backlog bound to the Sprint
        backlog = self.bmm.create(name="Sprint-Backlog", 
                                  ticket_types=[Type.USER_STORY, 
                                                Type.TASK],
                                  sorting_keys=[(Key.STORY_POINTS, 
                                                 SortOrder.DESCENDING),
                                                 (Key.REMAINING_TIME, 
                                                  SortOrder.ASCENDING)],
                                  b_type=BacklogType.SPRINT)
        # The Backlog should contains only the items planned for the Sprint, and with parents
        # planned for the Sprint too
        backlog1 =  self.bmm.get(name="Sprint-Backlog", scope=sprint1.name)
        self.assertEqual(backlog1.count(), 4)
        backlog2 =  self.bmm.get(name="Sprint-Backlog", scope=sprint2.name)
        self.assertEqual(backlog2.count(), 3)
    
    def testGlobalBacklogWithStrictOption(self):
        """Tests a global backlog with the Strict option"""
        backlog = self.bmm.create(name="Bug-Backlog", 
                                  ticket_types=[Type.BUG, Type.TASK],
                                  sorting_keys=[(Key.PRIORITY, 
                                                 SortOrder.ASCENDING)],
                                  b_type=BacklogType.GLOBAL,
                                  b_strict=True)
        # Build a hierarchy of Bug tasks
        b1 = self.teh.create_ticket(Type.BUG)
        t1 = self.teh.create_ticket(Type.TASK, 
                                    props={Key.REMAINING_TIME: '3'})
        t2 = self.teh.create_ticket(Type.TASK, 
                                    props={Key.REMAINING_TIME: '7'})
        # Link the Bug only with one task
        self.assertTrue(b1.link_to(t1))
        self.assertEqual(None, b1[Key.SPRINT])
        # Standard trac fields must not be None (see property change rendering
        # for ticket preview)
        self.assertEqual('', b1[Key.MILESTONE])
        self.assertEqual(Type.BUG, b1[Key.TYPE])
        self.assertEqual(None, t1[Key.SPRINT])
        self.assertEqual('', t1[Key.MILESTONE])
        self.assertEqual(None, t2[Key.SPRINT])
        self.assertEqual('', t2[Key.MILESTONE])
        
        # Now load the backlog, and check that even with strict
        # a global backlog shows all the tickets
        backlog.reload()
        self.assertTrue(backlog.b_strict)
        if backlog.count() != 3:
            print_backlog(backlog)
            self.fail("Backlog count wrong! %s != 3" % \
                       backlog.count())
        # Now links also the second task
        self.assertTrue(b1.link_to(t2))
        # Now reload the backlog and check if the second task is there too
        backlog.reload()
        self.assertTrue(backlog.b_strict)
        self.assertEqual(backlog.count(), 3)
        # Now plan the a task for a sprint so that should disappear from the
        # backlog
        s = self.teh.create_sprint("Test")
        t1[Key.SPRINT] = s.name
        self.assertTrue(t1.save_changes('Tester', 'Planned...'))
        backlog.reload()
        self.assertTrue(backlog.b_strict)
        self.assertEqual(backlog.count(), 2)
        # Now change the option strict to False and all three items 
        # should be there again
        backlog.b_strict = False
        self.assertTrue(backlog.save())
        backlog.reload()
        self.assertFalse(backlog.b_strict)
        self.assertEqual(backlog.count(), 3)
    
    def testScopedBacklogWithStrictOption(self):
        """Tests a scoped backlog with the Strict option"""
        s = self.teh.create_sprint("Test")
        backlog = self.bmm.create(name="Bug-Backlog", 
                                  ticket_types=[Type.BUG, Type.TASK],
                                  sorting_keys=[(Key.PRIORITY, 
                                                 SortOrder.ASCENDING)],
                                  b_strict=True, 
                                  b_type=BacklogType.SPRINT)
        # Build a hierarchy of Bug tasks
        b1 = self.teh.create_ticket(Type.BUG)
        t1 = self.teh.create_ticket(Type.TASK, 
                                    props={Key.REMAINING_TIME: '3'})
        t2 = self.teh.create_ticket(Type.TASK, 
                                    props={Key.REMAINING_TIME: '7'})
        # Link the Bug only with one task
        self.assertTrue(b1.link_to(t1))
        # Now load the backlog, and check that there is no ticket,
        # cause only the one explicitly planned for the scope should appear
        backlog = self.bmm.get(name="Bug-Backlog", scope=s.name)
        self.assertTrue(backlog.b_strict)
        self.assertEqual(backlog.count(), 0)
        # Now plan the bug for the sprint so that should appear in the
        # backlog without the linked task, cause not explicitly planned
        b1[Key.SPRINT] = s.name
        self.assertTrue(b1.save_changes('Tester', 'Planned...'))
        self.assertTrue(backlog.reload())
        self.assertTrue(backlog.b_strict)
        self.assertTrue(backlog.has_ticket(b1))
        self.assertEqual(backlog.count(), 1)
        # Now change the option strict to False and all the tasks linked to
        # bug1 should be there
        backlog.b_strict = False
        self.assertTrue(backlog.save())
        self.assertTrue(backlog.reload())
        self.assertFalse(backlog.b_strict)
        self.assertTrue(b1.is_linked_to(t1))
        self.assertTrue(backlog.has_ticket(b1))
        self.assertTrue(backlog.has_ticket(t1))
        self.assertEqual(backlog.count(), 2)
        # Now links also the second task
        self.assertTrue(b1.link_to(t2))
        # Now reload the backlog and check if the second task is there too
        self.assertTrue(backlog.reload())
        self.assertFalse(backlog.b_strict)
        self.assertEqual(backlog.count(), 3)
        
    def testScopedBacklogWithClosedTicket(self):
        """Tests if a scoped backlog loads also closed tickets"""
        
        sprint1 = self.teh.create_sprint("Sprint Scoped")
        sprint1.save()
        # Creates the Backlog bound to a scope (Sprint)
        backlog = self.bmm.create(name="Scoped-Backlog", 
                                  ticket_types=[Type.USER_STORY, 
                                                Type.TASK],
                                  sorting_keys=[(Key.STORY_POINTS, 
                                                 SortOrder.DESCENDING),
                                                 (Key.REMAINING_TIME, 
                                                  SortOrder.ASCENDING)],
                                  b_type=BacklogType.SPRINT)
        # Create 1 ticket
        task = self.teh.create_ticket(Type.TASK, 
                                      props={Key.REMAINING_TIME: '12',
                                             Key.SPRINT: sprint1.name})
        # Force reload
        backlog = self.bmm.get(name="Scoped-Backlog", 
                               scope=sprint1.name)
        self.assertTrue(backlog.has_ticket(task))
        self.assertEqual(backlog.count(), 1)
        task[Key.STATUS] = Status.CLOSED
        task.save_changes('tester', 'Changed Status')
        # Now should still be there even if closed, because the backlog is scoped
        backlog.reload()
        self.assertTrue(backlog.has_ticket(task))
        self.assertEqual(backlog.count(), 1)
        
    def testDeleteBacklog(self):
        """Tests the deletion of a Backlog"""
        # Creates the Backlog bound to a scope (Sprint)
        backlog = self.bmm.create(name="Scoped-Backlog", 
                                  ticket_types=[Type.USER_STORY, 
                                                Type.TASK],
                                  sorting_keys=[(Key.STORY_POINTS, 
                                                 SortOrder.DESCENDING),
                                                (Key.REMAINING_TIME, 
                                                 SortOrder.ASCENDING)],
                                  b_type=BacklogType.SPRINT)
        # Test that the backlog exists
        try:
            b2 = self.bmm.get(name="Scoped-Backlog")
            self.assertTrue(b2.delete())
        except Exception, e:
            print "Error: %s" % unicode(e)
            self.fail("Not able to load backlog!!!")
        try:
            b2 = self.bmm.get(self.env, "Scoped-Backlog")
            self.fail("The Backlog was not deleted!!!")
        except:
            self.assertTrue(True)
        
    def testRemoveItemFromBacklog(self):
        """Test the remove of an item from a Backlog"""
        t1 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '8'})
        t2 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '5'})
        backlog = self.bmm.create(name="Backlog",
                                  ticket_types=[Type.USER_STORY],
                                  sorting_keys=[(Key.STORY_POINTS, 
                                                 SortOrder.DESCENDING)])
        backlog.save()
        self.assertEqual(backlog.count(), 2)
        # Now remove t1
        self.assertTrue(backlog.remove(t1))
        self.assertFalse(backlog.has_ticket(t1))
        self.assertTrue(backlog.has_ticket(t2))
        
    def testRemoveFromBacklogsWhenClosed(self):
        """Test the remove from a backlog when the ticket gets closed and the
        backlog is global"""
        s = self.teh.create_sprint('Test')
        t1 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '8'})
        t2 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '5', Key.SPRINT: s.name})
        b1 = self.bmm.create(name="Backlog",
                             ticket_types=[Type.USER_STORY],
                             sorting_keys=[(Key.STORY_POINTS, 
                                            SortOrder.DESCENDING)])
        b1.save()
        self.assertEqual(b1.count(), 2)
        self.assertTrue(b1.has_ticket(t1))
        self.assertTrue(b1.has_ticket(t2))
        b2 = self.bmm.create(name="Scoped",
                             ticket_types=[Type.USER_STORY],
                             sorting_keys=[(Key.STORY_POINTS,
                                            SortOrder.DESCENDING)],
                             b_type=BacklogType.SPRINT)
        b2 = self.bmm.get(name="Scoped", scope=s.name)
        self.assertEqual(b2.count(), 1)
        self.assertTrue(b2.has_ticket(t2))
        self.assertFalse(b2.has_ticket(t1))
        b2.save()
        # Now close the tickets, should go away from the b1 and remain in b2
        t1[Key.STATUS] = t2[Key.STATUS] = Status.CLOSED
        t1.save_changes('tester', 'closed t1 ticket...')
        t2.save_changes('tester', 'closed t2 ticket...')
        # Reload Backlogs
        b1.reload()
        b2.reload()
        self.assertFalse(b1.has_ticket(t1))
        self.assertFalse(b1.has_ticket(t2))
        self.assertFalse(b2.has_ticket(t1))
        self.assertTrue(b2.has_ticket(t2))
        # Now remove directly a BacklogItem
        for bi in b1:
            bi[Key.STATUS] = Status.CLOSED
            b1.save()
            self.assertFalse(b1.has_ticket(bi.ticket),
                             "Ticket %s still in backlog!!!" % bi.ticket)


    def testSprintBacklogSaveLoosingOrderAfterSorting(self):
        """
        Tests the stability of the sorting after saving tickets in the Sprint Backlog.
        See Bug #281.
        """
        backlog = self._create_sprint_backlog()
        # Sort the backlog and store ticket position numbers
        backlog.sort()
        backlog.save()
        sorted_backlog = dict()
        bi_task = None
        for bi in backlog:
            sorted_backlog[bi.id] = bi.pos
            if bi.ticket.get_type() == Type.TASK:
                bi_task = bi
        # Change an item and save the backlog
        self.assertNotEqual(bi_task, None)
        bi_task[Key.REMAINING_TIME] = '100'
        # Now save the backlog
        backlog.save()
        self.assertEqual(backlog.get_item_by_id(bi_task.id)[Key.REMAINING_TIME],
                         '100')
        for bi in backlog:
            self.assertEqual(bi.pos, sorted_backlog[bi.id])
        
    def testProductBacklogSaveLoosingOrderAfterSorting(self):
        """
        Tests the stability of the sorting after saving tickets in the Product Backlog.
        See Bug #281.
        """
        backlog = self._create_product_backlog()
        # Sort the backlog and store ticket position numbers
        backlog.sort()
        self.assertEqual(14, backlog.count())
        backlog.save()
        backlog.reload()
        self.assertEqual(14, backlog.count())
        sorted_backlog = dict()
        bi_story = None
        for bi in backlog:
            sorted_backlog[bi.id] = bi.pos
            if bi.ticket.get_type() == Type.USER_STORY:
                bi_story = bi # just a story
        # Change an item and save the backlog
        self.assertNotEqual(bi_story, None)
        bi_story[Key.STORY_POINTS] = '13'
        # Now save the backlog
        backlog.save()
        backlog.reload()
        self.assertEqual(backlog.get_item_by_id(bi_story.id)[Key.STORY_POINTS],
                         '13')
        for bi in backlog:
            self.assertEqual(bi.pos, sorted_backlog[bi.id])
    
    def testTicketsUpdateFromBacklog(self): 
        """Tests that updating multiple tickets updates""" 
        sprint_backlog = self._create_sprint_backlog() 
        old_values = {} 
        for item in sprint_backlog: 
            if item[Key.TYPE] == Type.TASK: 
                rem_time = int(item[Key.REMAINING_TIME] or 0) 
                old_values[item[Key.ID]] = str(rem_time) 
                item[Key.REMAINING_TIME] = str(rem_time + 1) 

        # Now save the backlog and check that the tickets really got 
        # updated 
        self.assertTrue(sprint_backlog.save(author='tester',  
                                            comment='Updated backlog')) 
        for item in sprint_backlog: 
            if item[Key.TYPE] == Type.TASK: 
                self.assertNotEqual(old_values[item[Key.ID]],  
                                    item[Key.REMAINING_TIME]) 
                # reload the ticket so we are sure it is saved in the 
                # db. 
                temp_ticket = self.teh.load_ticket(item.ticket) 
                self.assertNotEqual(old_values[item[Key.ID]],  
                                    temp_ticket[Key.REMAINING_TIME]) 
    
    def testSortingEmptyBacklogDoesNotCauseException(self):
        backlog = self.bmm.create(name='FooBacklog', 
                                  ticket_types=['NotExistent'], 
                                  sorting_keys=[Key.PRIORITY], 
                                  b_type=BacklogType.GLOBAL, 
                                  b_strict=True)
        backlog.sort()


class TestReleaseBacklog(unittest.TestCase):
    """Tests the Release Backlog"""
    
    def setUp(self):
        self.teh = TestEnvHelper()
        self.env = self.teh.get_env()
        self.bmm = BacklogModelManager(self.env)
    
    def test_backlog_shows_right_tickets(self):
        """Tests the Release Backlog shows the Requirements belonging to a 
        specific Milestone"""
        sprint = self.teh.create_sprint("Release Sprint")
        release_backlog = self.teh.create_backlog("Release Backlog", 
                                                  b_type=BacklogType.MILESTONE, 
                                                  scope=sprint.milestone)
        count = release_backlog.count()
        req = self.teh.create_ticket(Type.REQUIREMENT, 
                                     {Key.MILESTONE: sprint.milestone})
        release_backlog.reload()
        self.assertTrue(release_backlog.has_ticket(req))
        self.assertEqual(count + 1, release_backlog.count())
        # Now add 2 stories to the requirement, one planned for the sprint one
        # not, only the one assigned to the sprint should appear in the backlog
        us1 = self.teh.create_ticket(Type.USER_STORY,
                                     {Key.SPRINT: sprint.name})
        us2 = self.teh.create_ticket(Type.USER_STORY)
        release_backlog.reload()
        self.assertTrue(release_backlog.has_ticket(us1))
        self.assertFalse(release_backlog.has_ticket(us2))
        self.assertEqual(count + 2, release_backlog.count())


class TestSprintBacklog(unittest.TestCase):
    """Specific tests for the Sprint Backlog"""

    def setUp(self):
        self.teh = TestEnvHelper()
        self.env = self.teh.get_env()
        self.bmm = BacklogModelManager(self.env)
    
    def test_tickets_from_other_sprint_not_appearing(self):
        """
        Tests that tasks created for other sprints are not appearing in the
        sprint backlog, see bug #345
        (https://svn.agile42.com/projects/agilo/ticket/345)
        """
        s = self.teh.create_sprint("Test")
        sb = self.teh.create_backlog("Sprint Backlog", 
                                     num_of_items=100,
                                     ticket_types=[Type.USER_STORY, Type.TASK],
                                     sorting_keys=[(Key.STORY_PRIORITY, SortOrder.DESCENDING),
                                                   (Key.REMAINING_TIME, SortOrder.DESCENDING)],
                                     b_type=BacklogType.SPRINT, 
                                     scope=s.name)
        self.assertEqual(100, sb.count())
        # get a ticket from the backlog and check that it is planned for the sprint
        self.assertEqual(s.name, sb.get_item_in_pos(10)[Key.SPRINT])
        # Now add an extra ticket
        task = self.teh.create_ticket(Type.TASK, props={Key.SPRINT: s.name,
                                                        Key.REMAINING_TIME: '2'})
        sb.reload()
        self.assertEqual(101, sb.count())
        self.assertTrue(sb.has_ticket(task), "Task %s not in backlog!!!" % task)
        # Now remove the ticket explicitly and check if the sprint field is set
        # to None
        self.teh.move_changetime_to_the_past([task])
        sb.remove(task)
        self.assertFalse(sb.has_ticket(task), "Task %s is in backlog!!!" % task)
        # reload task and backlog, the remove should have saved the task
        task = self.teh.load_ticket(task)
        sb.reload()
        self.assertFalse(sb.has_ticket(task), "Task %s is in backlog!!!" % task)
        self.assertEqual(None, task[Key.SPRINT])
        # Now move the ticket to another sprint
        s2 = self.teh.create_sprint("Another Sprint")
        task[Key.SPRINT] = s2.name
        task.save_changes('tester', 'Moved to sprint %s' % s2.name, 
                          when=to_datetime(None) + timedelta(seconds=1))
        self.assertEqual(s2.name, task[Key.SPRINT])
        # Now should not be in the backlog anymore
        sb.reload()
        self.assertFalse(sb.has_ticket(task), "Task %s is in backlog!!!" % task)
        # Now change sprint again, twice
        task[Key.SPRINT] = s.name
        task.save_changes('tester', 'Moved to sprint %s' % s.name, 
                          when=to_datetime(None) + timedelta(seconds=2))
        sb.reload()
        self.assertTrue(sb.has_ticket(task), "Task %s not in backlog!!!" % task)
        # again
        task[Key.SPRINT] = s2.name
        task.save_changes('tester', 'Moved to sprint %s' % s2.name, 
                          when=to_datetime(None) + timedelta(seconds=3))
        self.assertEqual(s2.name, task[Key.SPRINT])
        # Now should not be in the backlog anymore
        sb.reload()
        self.assertFalse(sb.has_ticket(task), "Task %s is in backlog!!!" % task)
    
    def test_referenced_requirements_are_displayed_in_the_sprint_backlog(self):
        """Test that referenced requirements are shown in the sprint backlog 
        even if they are planned for a milestone."""
        milestone = self.teh.create_milestone('1.0')
        sprint = self.teh.create_sprint("First Sprint")
        backlog_name = 'My Backlog'
        backlog = self.bmm.create(name=backlog_name, 
                                  ticket_types=[Type.USER_STORY, 
                                                Type.TASK, 
                                                Type.REQUIREMENT],
                                  sorting_keys=[Key.BUSINESS_VALUE], 
                                  b_type=BacklogType.SPRINT,
                                  b_strict=False)
        req = self.teh.create_ticket(Type.REQUIREMENT, {Key.SUMMARY: 'Requirement', Key.MILESTONE: milestone.name})
        story = self.teh.create_ticket(Type.USER_STORY, {Key.SUMMARY: 'Story', Key.SPRINT: sprint.name})
        req.link_to(story)
        
        backlog = self.bmm.get(name=backlog_name, scope=sprint.name)
        self.assertEqual(2, backlog.count())


class TestBacklogLevelSorting(unittest.TestCase):
    """Tests all the helper methods for the backlog level sorting"""
    def setUp(self):
        self.teh = TestEnvHelper()
        self.env = self.teh.get_env()
        self.bmm = BacklogModelManager(self.env)
        
    def test_tickets_levels_calculation(self):
        """Tests the ticket levels calculation"""
        b3l = self.bmm.create(name="3Levels", 
                              ticket_types=[Type.USER_STORY, 
                                            Type.TASK, 
                                            Type.REQUIREMENT, 
                                            Type.BUG],
                      sorting_keys=[(Key.PRIORITY, 
                                     SortOrder.DESCENDING)], 
                      b_strict=True,
                      b_type=BacklogType.SPRINT)
        self.assertTrue(b3l.save())
        # sprint
        s = self.teh.create_sprint("3l Sprint")
        # Now create some tickets
        r1 = self.teh.create_ticket(Type.REQUIREMENT)
        r2 = self.teh.create_ticket(Type.REQUIREMENT)
        s1 = self.teh.create_ticket(Type.USER_STORY, props={Key.SPRINT: s.name})
        self.assertTrue(r1.link_to(s1))
        s2 = self.teh.create_ticket(Type.USER_STORY, props={Key.SPRINT: s.name})
        t1 = self.teh.create_ticket(Type.TASK, props={Key.SPRINT: s.name})
        self.assertTrue(s1.link_to(t1))
        # Now calculate the levels
        b3l = self.bmm.get(name="3Levels", scope=s)
        # should return a list of tuples with ticket, level
        parents = list()
        t1, level = b3l._calculate_ticket_level(t1, parents)
        self.assertEqual(2, level)
        self.assertEqual(2, len(parents))
        self.assertEqual(Type.REQUIREMENT, parents[0][0].get_type())
        self.assertEqual(0, parents[0][1])
        self.assertEqual(Type.USER_STORY, parents[1][0].get_type())
        self.assertEqual(1, parents[1][1])
        
    def test_ticket_parent_child_wrong_order(self):
        """
        Tests whether the sorting rebuilds the blacktree to reorder parent
        child relationship
        """
        s = self.teh.create_sprint("TestMeSprint")
        # Now first a task, and than a story
        t = self.teh.create_ticket(Type.TASK, props={Key.SPRINT: s.name})
        b = self.bmm.get(name="Sprint Backlog", scope=s.name)
        self.assertTrue(b.has_ticket(t), "Task %s not in Backlog!!!" % t)
        # Now add a story and link it to the task
        us = self.teh.create_ticket(Type.USER_STORY, props={Key.SPRINT: s.name})
        b.reload()
        self.assertTrue(b.has_ticket(t), "Task %s not in Backlog!!!" % t)
        self.assertTrue(b.has_ticket(us), "User Story %s not in Backlog!!!" % us)
        self.assertTrue(us.link_to(t), "Link creation failed! %s -> %s" % \
                        (us, t))
        self.assertTrue(us.is_linked_to(t), "User Story not linked?!")
        b.sort()
        # Now the task should be after the story
        us_pos, t_pos = b.get_pos_of_ticket(us), b.get_pos_of_ticket(t)
        self.assertTrue(t_pos > us_pos,
                        "Task before its story? task: %s < story: %s" % \
                        (t_pos, us_pos))
    
    def _create_backlog_for_sorting_with_bugs(self, sprint):
        backlog_name = '%sBacklog' % sprint.name
        # We have to add some bugs in between to trigger the bug
        task = self.teh.create_ticket(Type.TASK, props={Key.SUMMARY: 'First Task',
                                                         Key.SPRINT: sprint.name})
        bug1 = self.teh.create_ticket(Type.BUG, props={Key.SUMMARY: 'Bug3', Key.SPRINT: sprint.name})
        requirement = self.teh.create_ticket(Type.REQUIREMENT, props={Key.SUMMARY: 'Requirement'})
        story2 = self.teh.create_ticket(Type.USER_STORY, props={Key.SUMMARY: 'Story',
                                                         Key.SPRINT: sprint.name})
        bug2 = self.teh.create_ticket(Type.BUG, props={Key.SUMMARY: 'My Bug', 
                                                      Key.SPRINT: sprint.name})
        story = self.teh.create_ticket(Type.USER_STORY, props={Key.SUMMARY: 'Story',
                                                         Key.SPRINT: sprint.name})
        
        requirement.link_to(story)
        story.link_to(task)
        
        requirement2 = self.teh.create_ticket(Type.REQUIREMENT, props={Key.SUMMARY: 'Requirement 2'})
        requirement2.link_to(story2)
        
        sprint_backlog = self.bmm.create(name=backlog_name, 
                                         ticket_types=[Type.USER_STORY,
                                                       Type.TASK, 
                                                       Type.REQUIREMENT, 
                                                       Type.BUG],
                                         sorting_keys=[(Key.BUSINESS_VALUE, 
                                                        SortOrder.DESCENDING)], 
                                         b_type=BacklogType.SPRINT,
                                         b_strict=False)
        
        sprint_backlog = self.bmm.get(name=backlog_name, scope=sprint.name)
        return (sprint_backlog, (requirement, story, task, requirement2, story2, bug1, bug2))
    
    def test_ticket_levels_in_backlog_calculated_correctly_if_bugs_are_shown(self):
        milestone = self.teh.create_milestone('FooRelease')
        sprint = self.teh.create_sprint("BugSortingSprint", milestone=milestone)
        sprint_backlog, items = self._create_backlog_for_sorting_with_bugs(sprint)
        (requirement, story1, task, requirement2, story2, bug1, bug2) = items
        
        def get_level_of(ticket):
            for item in sprint_backlog:
                if item[Key.ID] == ticket.id:
                    return item.level
            return None
        
        sprint_backlog.sort()
        self.assertEqual(sprint_backlog.count(), 7)
        self.assertEqual(0, get_level_of(requirement))
        self.assertEqual(0, get_level_of(requirement2))
        self.assertEqual(1, get_level_of(story1))
        self.assertEqual(1, get_level_of(story2))
        self.assertEqual(2, get_level_of(task))
        self.assertEqual(0, get_level_of(bug1))
        self.assertEqual(0, get_level_of(bug2))


def enable_backlog_filter(env, attribute_name):
    env.config.set('agilo-general', 'backlog_filter_attribute', attribute_name)
    env.config.save()
    assert AgiloConfig(env).backlog_filter_attribute == attribute_name


def add_attribute_as_task_field(env, attribute_name):
    config = env.config
    task_fields = config.get('agilo-types', Type.TASK)
    if attribute_name not in task_fields:
        new_fields = task_fields + ',' + attribute_name
        config.set('agilo-types', Type.TASK, new_fields)
        config.save()


class TestBacklogCanHideItemsByAttribute(unittest.TestCase):
    
    def _create_ticket(self, sprint, t_type=Type.TASK, **kwargs):
        attributes = {Key.SUMMARY: 'Task', Key.SPRINT: sprint.name}
        attributes.update(kwargs)
        ticket = self.teh.create_ticket(t_type, props=attributes)
        return ticket
    
    def _create_tasks_for_backlog(self):
        sprint = self.teh.create_sprint("FilteringSprint")
        self.tasks = [self._create_ticket(sprint, **{Key.COMPONENT: 'foo', Key.REMAINING_TIME: '1'}),
                      self._create_ticket(sprint, **{Key.COMPONENT: 'bar', Key.REMAINING_TIME: '2'}), 
                      self._create_ticket(sprint, **{Key.REMAINING_TIME: '3'}), ]
        self.sprint = sprint
    
    def setUp(self):
        self.teh = TestEnvHelper()
        self.env = self.teh.get_env()
        add_attribute_as_task_field(self.env, Key.COMPONENT)
        self._create_tasks_for_backlog()
        enable_backlog_filter(self.env, Key.COMPONENT)
        self.bmm = BacklogModelManager(self.env)
    
    def is_visible_in_backlog(self, backlog, task):
        for bi in backlog:
            ticket = bi.ticket
            if ticket.id == task.id:
                return bi.is_visible
        raise ValueError('Ticket #%d not found in backlog' % task.id)
    
    def test_backlog_can_hide_items_by_attribute(self):
        b = self.bmm.get(name="Sprint Backlog", 
                         scope=self.sprint.name, 
                         filter_by='foo')
        for task in self.tasks:
            is_visible = self.is_visible_in_backlog(b, task)
            if task[Key.COMPONENT] != 'bar':
                self.assertTrue(is_visible, task)
            else:
                self.assertFalse(is_visible, task)
    
    def test_remaining_time_only_includes_visible_items_if_filtered(self):
        self.assertEqual(1+2+3, self.get_total_remaining_time(self.sprint))
        b = self.bmm.get(name="Sprint Backlog", 
                         scope=self.sprint.name, 
                         filter_by='foo',
                         reload=True)
        self.assertEqual(1+3, self.get_total_remaining_time(self.sprint, tickets=b))
    
    def _create_team_with_rtusp_ratio(self):
        team = self.teh.create_team('FilteredTeam')
        self.sprint.team = team
        self.sprint.save()
        metrics = TeamMetrics(self.env, self.sprint)
        metrics[Key.RT_USP_RATIO] = 2
        metrics.save()
    
    def get_total_remaining_time(self, sprint, tickets=None):
        cmd = SprintController.GetTotalRemainingTimeCommand(self.env, 
                                                            sprint=sprint, 
                                                            tickets=tickets)
        return SprintController(self.env).process_command(cmd)
    
    def test_remaining_time_is_correct_for_stories_as_well(self):
        """Test that the remaining time calculation for a sprint is 
        correct even though all tasks of a story are filtered. The 
        estimated remaining time of the story is not used for the 
        remaining time of the sprint."""
        self._create_team_with_rtusp_ratio()
        story = self._create_ticket(self.sprint, Type.USER_STORY, 
                                    **{Key.STORY_POINTS: '10'})
        story.link_to(self.tasks[1])
        self.assertEqual(10*2, story[Key.ESTIMATED_REMAINING_TIME])
        self.assertEqual(1+2+3, self.get_total_remaining_time(self.sprint))
        b = self.bmm.get(name="Sprint Backlog", 
                         scope=self.sprint.name, 
                         filter_by='foo')
        self.assertEqual(1+0+3, self.get_total_remaining_time(self.sprint, 
                                                              tickets=b))


if __name__ == '__main__':
    #suite = unittest.TestLoader().loadTestsFromTestCase(TestBacklogLevelSorting)
    suite = unittest.TestSuite()
    suite.addTest(TestBacklog('testGlobalBacklogWithStrictOption'))
    unittest.TextTestRunner(verbosity=0).run(suite)
