#!/usr/bin/env python

import binascii
from collections import OrderedDict
import unittest
import argparse
import re
import subprocess
import sys
import os.path

import vecparser

cli_binary = ""

SUPPORTED_ALGORITHMS = [
    'AES-128/CFB',
    'AES-192/CFB',
    'AES-256/CFB',
    'AES-128/GCM',
    'AES-192/GCM',
    'AES-256/GCM',
    'AES-128/OCB',
    'AES-128/XTS',
    'AES-256/XTS',
    'ChaCha20Poly1305',
]

def append_ordered(base, additional_elements):
    for key in additional_elements:
        value = additional_elements[key]
        base[key] = value

class TestSequence(unittest.TestCase):
    pass

def create_test(data):
    def do_test_expected(self):
        iv = data['Nonce']
        key = data['Key']
        ad = data['AD'] if 'AD' in data else ""
        plaintext = data['In'].lower()
        ciphertext = data['Out'].lower()
        algorithm = data['Algorithm']
        direction = data['Direction']

        # CFB
        if algorithm == "AES-128/CFB":
            mode = "aes-128-cfb"
        elif algorithm == "AES-192/CFB":
            mode = "aes-192-cfb"
        elif algorithm == "AES-256/CFB":
            mode = "aes-256-cfb"
        # GCM
        elif algorithm == "AES-128/GCM":
            mode = "aes-128-gcm"
        elif algorithm == "AES-192/GCM":
            mode = "aes-192-gcm"
        elif algorithm == "AES-256/GCM":
            mode = "aes-256-gcm"
        # OCB
        elif algorithm == "AES-128/OCB":
            mode = "aes-128-ocb"
        # XTS
        elif algorithm == "AES-128/XTS":
            mode = "aes-128-xts"
        elif algorithm == "AES-256/XTS":
            mode = "aes-256-xts"
        # ChaCha20Poly1305
        elif algorithm == "ChaCha20Poly1305":
            mode = "chacha20poly1305"
        else:
            raise Exception("Unknown algorithm: '" + algorithm + "'")

        cmd = [
            cli_binary,
            "encryption",
            "--mode=%s" % mode,
            "--iv=%s" % iv,
            "--ad=%s" % ad,
            "--key=%s" % key]
        if direction == "decrypt":
            cmd += ['--decrypt']
        # out_raw = subprocess.check_output(cmd)

        if direction == "decrypt":
            invalue = ciphertext
        else:
            invalue = plaintext

        #print(cmd)

        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
        out_raw = p.communicate(input=binascii.unhexlify(invalue))[0]
        out = binascii.hexlify(out_raw).decode("UTF-8").lower()

        # Renamings
        if direction == "decrypt":
            expected = plaintext
        else:
            expected = ciphertext
        actual = out
        self.assertEqual(expected, actual)
    return do_test_expected

def get_testdata(document, max_tests):
    out = OrderedDict()
    for algorithm in document:
        if algorithm in SUPPORTED_ALGORITHMS:
            testcase_number = 0
            for testcase in document[algorithm]:
                testcase_number += 1
                for direction in ['encrypt', 'decrypt']:
                    testname = "{} no {:0>3} ({})".format(
                        algorithm.lower(), testcase_number, direction)
                    testname = re.sub("[^a-z0-9-]", "_", testname)
                    testname = re.sub("_+", "_", testname)
                    testname = testname.strip("_")
                    out[testname] = {}
                    for key in testcase:
                        value = testcase[key]
                        out[testname][key] = value
                    out[testname]['Algorithm'] = algorithm
                    out[testname]['Direction'] = direction

                if max_tests > 0 and testcase_number > max_tests:
                    break
    return out

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="")
    parser.add_argument('cli_binary',
                        help='path to the botan cli binary')
    parser.add_argument('--max-tests', type=int, default=20)
    parser.add_argument('unittest_args', nargs="*")
    args = parser.parse_args()

    cli_binary = args.cli_binary
    max_tests = args.max_tests

    vecfile_cfb = vecparser.VecDocument(os.path.join('src', 'tests', 'data', 'modes', 'cfb.vec'))
    vecfile_gcm = vecparser.VecDocument(os.path.join('src', 'tests', 'data', 'aead', 'gcm.vec'))
    vecfile_ocb = vecparser.VecDocument(os.path.join('src', 'tests', 'data', 'aead', 'ocb.vec'))
    vecfile_xts = vecparser.VecDocument(os.path.join('src', 'tests', 'data', 'modes', 'xts.vec'))
    vecfile_chacha20poly1305 = vecparser.VecDocument(os.path.join('src', 'tests', 'data', 'aead', 'chacha20poly1305.vec'))
    #data = vecfile.get_data()
    #for algo in data:
    #    print(algo)
    #    i = 0
    #    for testcase in data[algo]:
    #        i += 1
    #        print(str(i) + ":", testcase)

    testdata = OrderedDict()
    append_ordered(testdata, get_testdata(vecfile_cfb.get_data(), max_tests))
    append_ordered(testdata, get_testdata(vecfile_gcm.get_data(), max_tests))
    append_ordered(testdata, get_testdata(vecfile_ocb.get_data(), max_tests))
    append_ordered(testdata, get_testdata(vecfile_xts.get_data(), max_tests))
    append_ordered(testdata, get_testdata(vecfile_chacha20poly1305.get_data(), max_tests))

    #for testname in testdata:
    #    print(testname)
    #    for key in testdata[testname]:
    #        print("    " + key + ": " + testdata[testname][key])
    for testname in testdata:
        test_method = create_test(testdata[testname])
        test_method.__name__ = 'test_%s' % testname
        setattr(TestSequence, test_method.__name__, test_method)

    # Hand over sys.argv[0] and unittest_args to the testing framework
    sys.argv[1:] = args.unittest_args
    unittest.main()
