#!/usr/bin/env python

# {{{ Dependencies
from __future__ import print_function
import sys
from args import Program
from login_manager import login, confirm
from jira_manager import Jira
# }}}

# {{{ Dependencies for printing coloured content to the console
try:
    from termcolor import colored
    def blue(message): return colored(message, 'blue')
    def green(message): return colored(message, 'green')
    def red(message): return colored(message, 'red')
    def yellow(message): return colored(message, 'yellow')
    def faint(message): return colored(message, 'white', attrs=['dark'])
    def on_green(message): return colored(message, 'green')
    def on_red(message): return colored(message, 'red')
    def blue_on_white(message): return colored(message, 'blue')
    def yellow_on_white(message): return colored(message, 'yellow')
except:
    def blue(message): return u'[{}]'.format(message)
    def green(message): return message
    def red(message): return message
    def yellow(message): return message
    def faint(message): return message
    def on_green(message): return message
    def on_red(message): return message
    def blue_on_white(message): return message
    def yellow_on_white(message): return message
# }}}

# {{{ Mapping from product code to product name
product_map = {
    'OS'   : 'Corda',
    'ENT'  : 'Corda Enterprise',
    'NS'   : 'ENM',
    'ENM'  : 'ENM',
    'TEST' : 'Corda', # for demo and test purposes
}
# }}}

# {{{ Map product name to Jira project name
project_map = {
    'Corda' : 'Corda',
    'Corda Enterprise'  : 'Corda Enterprise',
    'ENM' : 'CENM'
}
# }}}

# {{{ JIRA queries
QUERY_LIST_TEST_CASES = \
    u'project = R3T AND type = "Platform Test Template" AND status = Active AND labels = "{}" ORDER BY key'
QUERY_LIST_TEST_INSTANCES = \
    u'project = R3T AND type = "Platform Test" AND labels = "{}" AND "Target Version/s" = "{}" ORDER BY key'
QUERY_LIST_TEST_INSTANCE_FOR_TICKET = \
    u'project = R3T AND type = "Platform Test" AND labels = "{}" AND "Target Version/s" = "{}" AND issue IN linkedIssues({})'
QUERY_LIST_ALL_TEST_RUNS_FOR_TICKET = \
    u'project = R3T AND type = "Platform Test Run" AND labels = "{}" AND parent = {} ORDER BY "Target Version/S"'
QUERY_LIST_TEST_RUN_FOR_TICKET = \
    u'project = R3T AND type = "Platform Test Run" AND labels = "{}" AND "Target Version/s" = "{}" AND parent = {}'
QUERY_LIST_BLOCKING_TEST_CASES = \
    u'project = R3T AND type = "Platform Test Template" AND labels = "{}" AND issue IN linkedIssues({}, "Blocks")'
# }}}

# {{{ list_test_cases() - List active test cases for a specific product
def list_test_cases(args):
    user, password = login('jira', args.user, use_keyring=not args.no_keyring, reset_keyring=args.reset_keyring)
    if not user or not password: sys.exit(1)
    jira = Jira().login(user, password)
    print(u'List of active test cases for {}:'.format(yellow(product_map[args.PRODUCT])))
    if args.verbose:
        print(faint('[{}]'.format(QUERY_LIST_TEST_CASES.format(args.PRODUCT))))
    print()
    has_tests = False
    for issue in jira.search(QUERY_LIST_TEST_CASES, args.PRODUCT):
        print(u' - {} {}'.format(blue(issue.key), issue.fields.summary))
        has_tests = True
    if not has_tests:
        print(u' - No active test cases found')
    print()
# }}}

# {{{ format_candidate() - Format a candidate number
def format_candidate(candidate):
    if candidate > 100:
        return '({})'.format(candidate)
    else:
        return 'RC{:02d}'.format(candidate)
# }}}

# {{{ show_status() - Show the status of all test runs for a specific release or release candidate
def show_status(args):
    user, password = login('jira', args.user, use_keyring=not args.no_keyring, reset_keyring=args.reset_keyring)
    if not user or not password: sys.exit(1)
    jira = Jira().login(user, password)
    version = '{} {}'.format(project_map[product_map[args.PRODUCT]], args.VERSION)
    candidate = '{} {}'.format(version, format_candidate(args.CANDIDATE)) if args.CANDIDATE else version
    if args.CANDIDATE:
        print(u'Status of test runs for {} version {} release candidate {}:'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow(format_candidate(args.CANDIDATE))))
    else:
        print(u'Status of test runs for {} version {}:'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION)))
    if args.verbose:
        print(faint('[{}]'.format(QUERY_LIST_TEST_INSTANCES.format(args.PRODUCT, version))))
    print()
    has_tests = False
    for issue in jira.search(QUERY_LIST_TEST_INSTANCES, args.PRODUCT, version):
        status = issue.fields.status['name'].lower()
        if status == 'pass':
            status = on_green('Pass')
        elif status == 'fail':
            status = on_red('Fail')
        elif status == 'descope':
            status = on_green('Descoped')
        else:
            status = ''
        print(u' - {} {} {}'.format(blue(issue.key), issue.fields.summary, status))
        has_test_runs = False
        if args.CANDIDATE:
            if args.verbose:
                print(faint('   [{}]'.format(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key))))
            run_list = jira.search(QUERY_LIST_TEST_RUN_FOR_TICKET, args.PRODUCT, candidate, issue.key)
        else:
            if args.verbose:
                print(faint('   [{}]'.format(QUERY_LIST_ALL_TEST_RUNS_FOR_TICKET.format(args.PRODUCT, issue.key))))
            run_list = jira.search(QUERY_LIST_ALL_TEST_RUNS_FOR_TICKET, args.PRODUCT, issue.key)
        for run in run_list:
            has_test_runs = True
            print()
            status = run.fields.status['name'].lower()
            if status == 'pass':
                status = on_green('Pass    ')
            elif status == 'fail':
                status = on_red('Fail    ')
            elif status == 'descope':
                status = on_green('Descoped')
            elif status == 'in progress':
                status = yellow_on_white('Active  ')
            else:
                status = blue_on_white('Open    ')
            print(u'    {} {} ({})'.format(status, faint(run.fields[jira.custom_fields_by_name['Target Version/s']][0]['name']), blue(run.key)))
        if not has_test_runs:
            print()
            print(u'    - No release candidate tests found')
        print()
        has_tests = True
    if not has_tests:
        print(u' - No test cases found for the specified release')
    print()
# }}}

# {{{ create_version() - Create a new JIRA version
def create_version(args):
    user, password = login('jira', args.user, use_keyring=not args.no_keyring, reset_keyring=args.reset_keyring)
    if not user or not password: sys.exit(1)
    jira = Jira().login(user, password)
    version = '{} {}'.format(project_map[product_map[args.PRODUCT]], args.VERSION)
    version = '{} {}'.format(version, format_candidate(args.CANDIDATE)) if args.CANDIDATE else version
    confirm(u'Create new version {}?'.format(yellow(version)), auto_yes=args.yes or args.dry_run)
    print()
    if not args.dry_run:
        for project in ['CORDA', 'ENT', 'ENM', 'R3T', 'CID']:
            print(u' - Creating version {} for project {} ...'.format(yellow(version), blue(project)))
            try:
                jira.jira.create_version(name=version, project=project, description=version)
                print(u'   {} - Created version for project {}'.format(green('SUCCESS'), blue(project)))
            except Exception as error:
                if args.verbose:
                    print(u'   {} - Failed to version: {}'.format(red('FAIL'), error))
                else:
                    print(u'   {} - Failed to version: {}'.format(red('FAIL'), error.text))
            print()


# }}}

# {{{ create_release() - Create test cases for a specific version of a product
def create_release(args):
    user, password = login('jira', args.user, use_keyring=not args.no_keyring, reset_keyring=args.reset_keyring)
    if not user or not password: sys.exit(1)
    jira = Jira().login(user, password)
    version = '{} {}'.format(project_map[product_map[args.PRODUCT]], args.VERSION)
    confirm(u'Create test cases for {} version {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION)), auto_yes=args.yes or args.dry_run)
    if args.verbose:
        print(faint('[{}]'.format(QUERY_LIST_TEST_CASES.format(args.PRODUCT))))
    print()
    has_tests = False
    for issue in jira.search(QUERY_LIST_TEST_CASES, args.PRODUCT):
        print(u' - {} {}'.format(blue(issue.key), issue.fields.summary))
        print()
        has_tests = True
        print(u'    - Creating test case for version {} ...'.format(yellow(args.VERSION)))
        if args.verbose:
            print(faint(u'      [{}]'.format(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, issue.key))))
        has_test_case_for_version = len(list(jira.search(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, issue.key))))
        if has_test_case_for_version:
            print(u'      {} - Test case for version already exists'.format(yellow('SKIPPED')))
        else:
            try:
                test_case = issue.clone(issuetype='Platform Test', version=version, dry_run=args.dry_run)
                print(u'      {} - Created ticket {}'.format(green('SUCCESS'), blue(test_case.key)))
            except Exception as error:
                print(u'      {} - Failed to create ticket: {}'.format(red('FAIL'), error))
            print()
            print(u'    - Linking test case to template ...')
            try:
                jira.link(issue.key, test_case.key, dry_run=args.dry_run)
                print(u'      {} - Linked {} to {}'.format(green('SUCCESS'), blue(issue.key), blue(test_case.key)))
            except Exception as error:
                print(u'      {} - Failed to link tickets: {}'.format(red('FAIL'), error))
        print()
    print(u'Copying links from test templates for {} version {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION)))
    print()
    for issue in jira.search(QUERY_LIST_TEST_CASES, args.PRODUCT):
        print(u' - {} {}'.format(blue(issue.key), issue.fields.summary))
        print()
        print(u'    - Copying links for test case {} ...'.format(blue(issue.key)))
        has_links = False
        if args.verbose:
            print(faint(u'      [{}]'.format(QUERY_LIST_BLOCKING_TEST_CASES.format(args.PRODUCT, issue.key))))
        for blocking_issue in jira.search(QUERY_LIST_BLOCKING_TEST_CASES, args.PRODUCT, issue.key):
            from_ticket = list(jira.search(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, issue.key)))
            to_ticket = list(jira.search(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, blocking_issue.key)))
            if len(from_ticket) == 0 or len(to_ticket) == 0:
                continue
            has_links = True
            from_key = from_ticket[0].key
            to_key = to_ticket[0].key
            try:
                jira.link(from_key, to_key, Jira.BLOCKS, dry_run=args.dry_run)
                print(u'      {} - Linked {} to {}'.format(green('SUCCESS'), blue(from_key), blue(to_key)))
            except Exception as error:
                print(u'      {} - Failed to link tickets {} and {}: {}'.format(red('FAIL'), blue(from_key), blue(to_key), error))
        if not has_links:
            print(u'      {} - No relevant links found for ticket {}'.format(yellow('SKIPPED'), blue(issue.key)))
        print()
    if not has_tests:
        print(u' - No active test cases found')
        print()
# }}}

# {{{ create_release_candidate() - Create test run tickets for a specific release candidate of a product
def create_release_candidate(args):
    user, password = login('jira', args.user, use_keyring=not args.no_keyring, reset_keyring=args.reset_keyring)
    if not user or not password: sys.exit(1)
    jira = Jira().login(user, password)
    version = '{} {}'.format(project_map[product_map[args.PRODUCT]], args.VERSION)
    CANDIDATE = args.CANDIDATE[0]
    candidate = '{} {}'.format(version, format_candidate(CANDIDATE))
    confirm(u'Create test run tickets for {} version {} release candidate {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow(format_candidate(CANDIDATE))), auto_yes=args.yes or args.dry_run)
    if args.verbose:
        print(faint('[{}]'.format(QUERY_LIST_TEST_INSTANCES.format(args.PRODUCT, version))))
    print()
    has_tests = False
    for issue in jira.search(QUERY_LIST_TEST_INSTANCES, args.PRODUCT, version):
        test_status = issue.fields.status['name']
        print(u' - {} {} ({})'.format(blue(issue.key), issue.fields.summary, test_status))
        epic_field = jira.custom_fields_by_name['Epic Link']
        epic = issue.fields[epic_field] if epic_field in issue.fields.to_dict() else ''
        labels = issue.fields.labels + [epic]
        if test_status in ['Pass', 'Fail', 'Descope']:
            print(u'   {} - Parent test is marked as {}'.format(yellow('SKIPPED'), test_status))
            print()
            continue
        print()
        has_tests = True
        print(u'    - Creating test run ticket for release candidate {} ...'.format(yellow(format_candidate(CANDIDATE))))
        if args.verbose:
            print(faint(u'      [{}]'.format(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key))))
        has_test_instance_for_version = len(list(jira.search(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key))))
        if has_test_instance_for_version:
            print(u'      {} - Ticket for release candidate already exists'.format(yellow('SKIPPED')))
        else:
            try:
                test_case = issue.clone(issuetype='Platform Test Run', version=candidate, parent=issue.key, labels=labels, dry_run=args.dry_run)
                print(u'      {} - Created ticket {}'.format(green('SUCCESS'), blue(test_case.key)))
            except Exception as error:
                print(u'      {} - Failed to create ticket: {}'.format(red('FAIL'), error))
        print()
    if not has_tests:
        print(u' - No active test cases found')
        print()
# }}}

# {{{ main() - Entry point
def main():
    with Program(description='tool for managing test cases and test runs in JIRA') as program:

        PRODUCTS = ['OS', 'ENT', 'NS', 'TEST']

        program.add('--verbose', '-v', help='turn on verbose logging', action='store_true')
        program.add('--yes', '-y', help='automatically answer "yes" to all prompts', action='store_true')
        program.add('--user', '-u', help='the user name or email address used to log in to JIRA', type=str, metavar='USER')
        program.add('--no-keyring', help='do not persist passwords in the keyring', action='store_true')
        program.add('--reset-keyring', help='reset passwords persisted in the keyring (if any)', action='store_true')

        def mixin_dry_run(command):
            command.add('--dry-run', '-d', help='run action without applying any changes to JIRA', action='store_true')

        def mixin_product(command):
            command.add('PRODUCT', help='the product under test (OS, ENT, NS)', choices=PRODUCTS, metavar='PRODUCT')

        def mixin_version_and_product(command):
            mixin_product(command)
            command.add('VERSION', help='the target version of the release, e.g., 4.0', type=str)

        def mixin_candidate(command, optional=False):
            if optional:
                nargs = '?'
            else:
                nargs = 1
            command.add('CANDIDATE', help='the number of the release candidate, e.g., 1 for RC01, or an 8 digit date in the format YYYYMMDD for snapshot releases', type=int, nargs=nargs)

        with program.command('list-tests', 'list test cases applicable to the provided specification', list_test_cases) as command:
            mixin_product(command)

        with program.command('status', 'show the status of all test runs for a specific release or release candidate', show_status) as command:
            mixin_version_and_product(command)
            mixin_candidate(command, True)

        with program.command('create-version', 'create a new version in JIRA', create_version) as command:
            mixin_dry_run(command)
            mixin_version_and_product(command)
            mixin_candidate(command, True)

        with program.command('create-release-tests', 'create test cases for a new release in JIRA', create_release) as command:
            mixin_dry_run(command)
            mixin_version_and_product(command)

        with program.command('create-release-candidate-tests', 'create test runs for a new release candidate in JIRA', create_release_candidate) as command:
            mixin_dry_run(command)
            mixin_version_and_product(command)
            mixin_candidate(command)
# }}}

if __name__ == '__main__': main()
