# Copyright 2017 Intel Corporation
#
# 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.
# ------------------------------------------------------------------------------

# pylint: disable=attribute-defined-outside-init

import unittest
import logging
import operator
import subprocess
import shlex
import urllib.request
import urllib.error
import json
from base64 import b64decode
import hashlib
import random

import cbor

from sawtooth_signing import create_context
from sawtooth_signing import CryptoFactory

from sawtooth_validator.protobuf.transaction_pb2 import Transaction
from sawtooth_validator.protobuf.transaction_pb2 import TransactionHeader
from sawtooth_validator.protobuf.batch_pb2 import Batch
from sawtooth_validator.protobuf.batch_pb2 import BatchList
from sawtooth_validator.protobuf.batch_pb2 import BatchHeader

from sawtooth_integration.tests.integration_tools import wait_for_rest_apis


LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)


INTKEY_PREFIX = '1cf126'
XO_PREFIX = '5b7349'
WAIT = 300


def make_intkey_address(name):
    return INTKEY_PREFIX + hashlib.sha512(
        name.encode('utf-8')).hexdigest()[-64:]


class TestTwoFamilies(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        wait_for_rest_apis(['rest-api:8008'])

    def test_two_families(self):
        '''
        After starting a validator with both intkey and xo
        transaction processors and initializing xo, verify that
        state is empty. Next, send the following pairs of commands,
        verifying that the state is as it should be after each:

        1. Send a batch of intkey 'set' txns and create an xo game.

        2. Send a batch of valid intkey 'inc'/'dec' txns and take an xo space.

        3. Send an invalid batch of intkey 'inc'/'dec' txns (invalid because
        they target names that haven't been set) and take the same xo space.

        4. Send more valid intkey txns and take a new xo space.

        5. Send the same intkey 'set' txns (which are now invalid) and
        create the same xo game (which has already been created).

        6. Send more valid intkey txns and take a new xo space.

        Besides verifying that the xo and intkey commands act as expected,
        verify that there is nothing in the state that isn't xo or intkey.
        '''
        context = create_context('secp256k1')
        signer = CryptoFactory(context).new_signer(
            context.new_random_private_key())

        self.intkey_verifier = IntkeyTestVerifier()
        self.xo_verifier = XoTestVerifier()

        _send_xo_cmd('sawtooth keygen')

        self.verify_empty_state()

        commands = zip(
            self.intkey_verifier.intkey_cmds,
            self.xo_verifier.xo_cmds)

        how_many_updates = 0

        for intkey_cmd, xo_cmd in commands:
            _send_intkey_cmd(signer, intkey_cmd)
            _send_xo_cmd('{} --url {} --wait {}'.format(
                xo_cmd,
                'http://rest-api:8008',
                WAIT))

            if intkey_cmd == self.intkey_verifier.valid_txns:
                how_many_updates += 1

            self.verify_state_after_n_updates(how_many_updates)

    def verify_empty_state(self):
        LOGGER.debug('Verifying empty state')

        self.assertEqual(
            [],
            _get_intkey_state(),
            'Expected intkey state to be empty')

        self.assertEqual(
            [],
            _get_xo_state(),
            'Expected xo state to be empty')

    def verify_state_after_n_updates(self, num):
        LOGGER.debug('Verifying state after %s updates', num)

        intkey_state = _get_intkey_data()
        LOGGER.info('Current intkey state: %s', intkey_state)
        xo_data = _get_xo_data()
        LOGGER.info('Current xo state: %s', xo_data)

        self.assertEqual(
            intkey_state,
            self.intkey_verifier.state_after_n_updates(num),
            'Wrong intkey state')

        self.assertEqual(
            xo_data,
            self.xo_verifier.state_after_n_updates(num),
            'Wrong xo state')


# sending commands

def _send_xo_cmd(cmd_str):
    LOGGER.info('Sending xo cmd')
    subprocess.run(
        shlex.split(cmd_str),
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=True)


def _send_intkey_cmd(signer, txns):
    batch = create_batch(signer, txns)
    LOGGER.info('Sending intkey txns')
    _post_batch(batch)


def create_batch(signer, triples):
    transactions = [
        create_transaction(signer, verb, name, value)
        for verb, name, value in triples
    ]

    txn_signatures = [txn.header_signature for txn in transactions]

    header = BatchHeader(
        signer_public_key=signer.get_public_key().as_hex(),
        transaction_ids=txn_signatures
    ).SerializeToString()

    signature = signer.sign(header)

    batch = Batch(
        header=header,
        transactions=transactions,
        header_signature=signature)

    batch_list = BatchList(batches=[batch])

    return batch_list.SerializeToString()


def create_transaction(signer, verb, name, value):
    payload = cbor.dumps({'Verb': verb, 'Name': name, 'Value': value},
                         sort_keys=True)

    addresses = [make_intkey_address(name)]

    nonce = hex(random.randint(0, 2**64))

    txn_pub_key = signer.get_public_key().as_hex()
    header = TransactionHeader(
        signer_public_key=txn_pub_key,
        family_name="intkey",
        family_version="1.0",
        inputs=addresses,
        outputs=addresses,
        dependencies=[],
        payload_sha512=hashlib.sha512(payload).hexdigest(),
        batcher_public_key=signer.get_public_key().as_hex(),
        nonce=nonce
    )

    signature = signer.sign(header.SerializeToString())

    return Transaction(
        header=header.SerializeToString(),
        payload=payload,
        header_signature=signature)


# rest_api calls

def _post_batch(batch):
    headers = {'Content-Type': 'application/octet-stream'}
    response = _query_rest_api(
        '/batches', data=batch, headers=headers, expected_code=202)
    return _submit_request('{}&wait={}'.format(response['link'], WAIT))


def _get_intkey_data():
    state = _get_intkey_state()
    # state is a list of dictionaries: { data: ..., address: ... }
    dicts = [cbor.loads(b64decode(entry['data'])) for entry in state]
    LOGGER.debug(dicts)
    data = {k: v for d in dicts for k, v in d.items()}  # merge dicts
    return data


def _get_xo_data():
    state = _get_xo_state()
    data = b64decode(state[0]['data']).decode().split('|')[0].split(',')
    game_name, board, turn, _, _ = data
    return board, turn, game_name


def _get_intkey_state():
    state = _get_state_prefix(INTKEY_PREFIX)
    return state


def _get_xo_state():
    state = _get_state_prefix(XO_PREFIX)
    return state


def _get_state_prefix(prefix):
    response = _query_rest_api('/state?address=' + prefix)
    return response['data']


def _query_rest_api(suffix='', data=None, headers=None, expected_code=200):
    if headers is None:
        headers = {}
    url = 'http://rest-api:8008' + suffix
    return _submit_request(
        urllib.request.Request(url, data, headers),
        expected_code=expected_code)


def _submit_request(request, expected_code=200):
    conn = urllib.request.urlopen(request)
    assert expected_code == conn.getcode()

    response = conn.read().decode('utf-8')
    return json.loads(response)

# verifiers


class XoTestVerifier:
    def __init__(self):
        self.xo_cmds = (
            'xo create game --wait {}'.format(WAIT),
            'xo take game 5 --wait {}'.format(WAIT),
            'xo take game 5 --wait {}'.format(WAIT),
            'xo take game 9 --wait {}'.format(WAIT),
            'xo create game --wait {}'.format(WAIT),
            'xo take game 4 --wait {}'.format(WAIT),
        )

    def state_after_n_updates(self, num):
        state = {
            0: ('---------', 'P1-NEXT', 'game'),
            1: ('----X----', 'P2-NEXT', 'game'),
            2: ('----X---O', 'P1-NEXT', 'game'),
            3: ('---XX---O', 'P2-NEXT', 'game')
        }

        try:
            return state[num]
        except KeyError:
            return ()


class IntkeyTestVerifier:
    def __init__(self):
        self.valid = 'ragdoll', 'sphynx', 'van'
        self.invalid = 'manx', 'persian', 'siamese'
        self.verbs = 'inc', 'dec', 'inc'
        self.sets = 'set', 'set', 'set'
        self.incdec = 11, 13, 10
        self.initial = 110, 143, 130

        self.populate = tuple(zip(self.sets, self.valid, self.initial))
        self.valid_txns = tuple(zip(self.verbs, self.valid, self.incdec))
        self.invalid_txns = tuple(zip(self.verbs, self.invalid, self.incdec))

        self.intkey_cmds = (
            self.populate,
            self.valid_txns,
            self.invalid_txns,
            self.valid_txns,
            self.populate,
            self.valid_txns,
        )

    def state_after_n_updates(self, num):
        ops = {
            'inc': operator.add,
            'dec': operator.sub
        }

        expected_values = [
            ops[verb](init, (val * num))
            for verb, init, val
            in zip(self.verbs, self.initial, self.incdec)
        ]

        return dict(zip(self.valid, expected_values))
