#!/usr/bin/python3
# -*- coding: utf-8 -*-

import nerve
import nerve.connect

import io
import json
import time
import os.path
import traceback
import mimetypes
import urllib.parse
import email.utils

def singleton(cls):
    return cls()

class QueryHandler (object):
    def print_result(self, result):
        rstr = str(result)
        nerve.log("result: " + ( rstr[:75] + '...' if len(rstr) > 75 else rstr ), logtype='debug')

    def query(self, _queryurl, *args, **kwargs):
        raise NotImplementedError("query is not implemented for this url scheme")

    def subscribe(self, topic, action, label=None, **eventmask):
        raise NotImplementedError("subscribe is not implemented for this url scheme")

    def unsubscribe(self, topic=None, action=None, label=None, **eventmask):
        raise NotImplementedError("unsubscribe is not implemented for this url scheme")

def delistify(kwargs):
    for name in kwargs.keys():
        if not name.endswith("[]") and isinstance(kwargs[name], list) and len(kwargs[name]) == 1:
            kwargs[name] = kwargs[name][0]
    return kwargs

class Request (object):
    def __init__(self, source, user, reqtype, urlstring, args, headers=dict()):
        self.source = source
        self.user = user
        self.reqtype = reqtype      # could be GET, POST, QUERY, CONNECT?
        self.url = urllib.parse.urlparse(urlstring) if type(urlstring) == str else urlstring
        self.args = self.add_query_args(self.url, args)
        self.headers = [ (key.lower(), item) for (key, item) in headers.items() ]

        self.segments = self.url.path.lstrip('/').split('/')
        self.current_segment = 0

    @staticmethod
    def add_query_args(url, kwargs=None):
        if not kwargs:
            kwargs = dict()
        kwargs.update(delistify(urllib.parse.parse_qs(url.query, keep_blank_values=True)))
        return kwargs

    @staticmethod
    def get_positional_args(args, kwargs):
        args = list(args)
        for i in sorted( int(key[1:]) for key in kwargs.keys() if key.startswith('$') ):
            if i != len(args):
                raise ValueError("incorrectly numbered positional argument: $" + str(i))
            args.append(kwargs['$' + str(i)])
            del kwargs['$' + str(i)]
        return args

    @staticmethod
    def put_positional_args(args, kwargs):
        for i in range(len(args)):
            #if '$'+str(i) in kwargs:
            #   raise ValueError("positional argument " + '$'+str(i) + " already exists in kwargs")
            kwargs['$'+str(i)] = args[i]

    def arg(self, name, default=None):
        if name in self.args:
            return self.args[name]
        return default

    def get_header(self, name, default=None):
        name = name.lower()
        for key, value in self.headers:
            if key == name:
                return value
        return default

    def get_header_all(self, name):
        name = name.lower()
        return [ value for key, value in self.headers if key == name ]

    def next_segment(self, default=None):
        if self.current_segment < len(self.segments):
            seg = self.segments[self.current_segment]
            self.current_segment += 1
            return seg
        return default

    def back_segment(self):
        if self.current_segment > 0:
            self.current_segment -= 1

    def segments_left(self):
        return len(self.segments) - self.current_segment

    def get_slug(self):
        if self.current_segment < len(self.segments):
            seg = '/'.join(self.segments[self.current_segment:])
            self.current_segment = len(self.segments)
            return seg
        return ''

    def get_host(self):
        host = self.get_header('Host', default=None)
        if host == None:
            host = 'localhost:' + str(self.source.get_setting('port'))
        return host

    def get_location(self, leaf=''):
        location = '/' + '/'.join(self.segments[:self.current_segment-1])
        if leaf:
            location += '/' + leaf
        return location

    def make_url(self, leaf=''):
        return 'http://' + self.get_host() + self.get_location(leaf)

class NotFoundError (Exception): pass
class ControllerError (Exception): pass

class Controller (nerve.ObjectNode):
    def __init__(self, **config):
        super().__init__(**config)
        self._headers = [ ]
        self._redirect = None
        self._error = None
        self._view = None
        self._request = None

    def initialize(self, request):
        self._headers = [ ]
        self._redirect = None
        self._error = None
        self._view = None
        self._request = request
        self._status = 0

    def finalize(self, request):
        pass

    def add_header(self, name, value):
        self._headers.append( (name, value) )

    def get_headers(self):
        return self._headers

    def set_view(self, view):
        self._view = view

    def load_plaintext_view(self, text):
        self.set_view(PlainTextView(text))

    def load_json_view(self, data):
        self.set_view(JsonView(data))

    def load_file_view(self, filename, base=None):
        """
        fullpath = os.path.join(base, filename) if base else filename
        mtime = int(os.path.getmtime(fullpath))
        since = self._request.get_header('If-Modified-Since', None)
        if since:
            since = int(time.mktime(email.utils.parsedate(since)))
            if mtime <= since:
                self._status = 304
                return
        #self.add_header('Cache-Control', 'max-age=604800')  # 7 days
        self.add_header('Cache-Control', 'max-age=2592000')  # 30 days
        self.add_header('Last-Modified', time.strftime("%a, %e %b %Y %H:%M:%S %z", time.localtime(mtime)))
        """
        self.set_view(FileView(filename, base))

    def get_mimetype(self):
        if not self._view:
            return None
        return self._view._mimetype

    def get_output(self):
        if not self._view:
            return None
        return self._view.get_output()

    def redirect_to(self, location):
        self._redirect = location

    def get_redirect(self):
        return self._redirect

    def get_status(self):
        return self._status

    def set_error(self, error):
        self._error = error

    def get_error(self):
        return self._error

    def get_request(self):
        return self._request

    def handle_request(self, request):
        self.initialize(request)
        try:
            self.do_request(request)
            if self._view:
                self._view.finalize()

        except Exception as e:
            try:
                self.handle_error(e, traceback.format_exc(), request)
            except:
                nerve.log("error while handling exception:\n" + traceback.format_exc(), logtype='error')

        finally:
            self.finalize(request)

        if self._error is None:
            return True
        return False

    def handle_error(self, error, traceback, request):
        self.set_error(error)
        if not self._view:
            self.load_plaintext_view('')
        nerve.log(traceback, logtype='error')
        #if 'text/html' in request.headers['accept']:
        #   render some html
        #else:
        #   self.write(traceback)
        self._view.write(traceback)

    def do_request(self, request):
        name = request.next_segment()
        if not name:
            name = 'index'

        try:
            method = getattr(self, name)
        except AttributeError:
            raise NotFoundError("Page not found: " + name) from None

        # TODO this should be added to make accessible only methods declared as public, but the permissions error causes a login prompt to show which we don't want in this case because no matter who
        #      tries to access the method, it should be denied
        if not nerve.is_public(method):
            raise nerve.users.UserPermissionsError("method is not public: " + name)
        method(request)

class View (nerve.ObjectNode):
    def __init__(self, **config):
        super().__init__(**config)
        self._mimetype = None
        self._encoding = 'utf-8'
        self._finalized = False
        self._output = None

    def get_mimetype(self):
        return (self._mimetype, self._encoding)

    def set_mimetype(self, mimetype):
        self._mimetype = mimetype

    def add_header(self, name, value):
        self._headers.append( (name, value) )

    def set_output(self, output):
        self._output = output

    def get_output(self):
        self.finalize()
        if self._output:
            return self._output
        return b''

    def __str__(self):
        self.finalize()
        if self._output:
            return self._output.decode(self._encoding)
        return "<no output>"

    def finalize(self):
        if not self._finalized:
            self._finalized = True
            self.render()

    def render(self):
        pass

class TextView (View):
    def __init__(self, **config):
        super().__init__(**config)
        self._output = io.StringIO()

    def write_bytes(self, data):
        self._output.write(data.decode(self._encoding))

    def write(self, text):
        self._output.write(text)

    def print(self, *args, **kwargs):
        kwargs['file'] = self._output
        print(*args, **kwargs)

    def get_output(self):
        self.finalize()
        return bytes(self._output.getvalue(), self._encoding)

    def __str__(self):
        self.finalize()
        return self._output.getvalue()

class PlainTextView (TextView):
    def __init__(self, text=None):
        super().__init__()
        self._mimetype = 'text/plain'
        if text != None:
            self.write(text)

class HTMLView (TextView):
    def __init__(self, **config):
        super().__init__(**config)
        self._mimetype = 'text/html'
        self._indent = 0

    def write(self, text, tab=0):
        if tab < 0:
            self._indent += tab
        super().write(('    ' * self._indent) + text)
        if tab > 0:
            self._indent += tab

    def writeln(self, text, tab=0):
        self.write(text + '\n', tab)

class JsonView (View):
    def __init__(self, data=None):
        super().__init__()
        self._mimetype = 'application/json'
        self._data = data

    def render(self):
        def json_default(obj):
            return str(obj)
        text = json.dumps(self._data, sort_keys=True, indent=4, separators=(',', ': '), default=json_default)
        self.set_output(bytes(text, self._encoding))

class FileView (View):
    def __init__(self, filename, base=None):
        super().__init__()
        self.filename = os.path.join(base, filename) if base else filename

    def render(self):
        (self._mimetype, self._encoding) = mimetypes.guess_type(self.filename)
        if self._encoding == None:
            self._encoding = 'utf-8'


#!/usr/bin/env python3
# Copyright (c) 2014-2019 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the rawtransaction RPCs.

Test the following RPCs:
   - createrawtransaction
   - signrawtransactionwithwallet
   - sendrawtransaction
   - decoderawtransaction
   - getrawtransaction
"""

from collections import OrderedDict
from decimal import Decimal
from io import BytesIO
from test_framework.messages import CTransaction, ToHex
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
    assert_equal,
    assert_raises_rpc_error,
    connect_nodes,
    hex_str_to_bytes,
)

class multidict(dict):
    """Dictionary that allows duplicate keys.

    Constructed with a list of (key, value) tuples. When dumped by the json module,
    will output invalid json with repeated keys, eg:
    >>> json.dumps(multidict([(1,2),(1,2)])
    '{"1": 2, "1": 2}'

    Used to test calls to rpc methods with repeated keys in the json object."""

    def __init__(self, x):
        dict.__init__(self, x)
        self.x = x

    def items(self):
        return self.x

# Create one-input, one-output, no-fee transaction:
class RawTransactionsTest(BitcoinTestFramework):
    def set_test_params(self):
        self.setup_clean_chain = True
        self.num_nodes = 3
        self.extra_args = [
            ["-txindex"],
            ["-txindex"],
            ["-txindex"],
        ]

    def skip_test_if_missing_module(self):
        self.skip_if_no_wallet()

    def setup_network(self):
        super().setup_network()
        connect_nodes(self.nodes[0], 2)

    def run_test(self):
        self.log.info('prepare some coins for multiple *rawtransaction commands')
        self.nodes[2].generate(1)
        self.sync_all()
        self.nodes[0].generate(101)
        self.sync_all()
        self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(),1.5)
        self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(),1.0)
        self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(),5.0)
        self.sync_all()
        self.nodes[0].generate(5)
        self.sync_all()

        self.log.info('Test getrawtransaction on genesis block coinbase returns an error')
        block = self.nodes[0].getblock(self.nodes[0].getblockhash(0))
        assert_raises_rpc_error(-5, "The genesis block coinbase is not considered an ordinary transaction", self.nodes[0].getrawtransaction, block['merkleroot'])

        self.log.info('Check parameter types and required parameters of createrawtransaction')
        # Test `createrawtransaction` required parameters
        assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction)
        assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [])

        # Test `createrawtransaction` invalid extra parameters
        assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [], {}, 0, False, 'foo')

        # Test `createrawtransaction` invalid `inputs`
        txid = '1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000'
        assert_raises_rpc_error(-3, "Expected type array", self.nodes[0].createrawtransaction, 'foo', {})
        assert_raises_rpc_error(-1, "JSON value is not an object as expected", self.nodes[0].createrawtransaction, ['foo'], {})
        assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[0].createrawtransaction, [{}], {})
        assert_raises_rpc_error(-8, "txid must be of length 64 (not 3, for 'foo')", self.nodes[0].createrawtransaction, [{'txid': 'foo'}], {})
        assert_raises_rpc_error(-8, "txid must be hexadecimal string (not 'ZZZ7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844')", self.nodes[0].createrawtransaction, [{'txid': 'ZZZ7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844'}], {})
        assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", self.nodes[0].createrawtransaction, [{'txid': txid}], {})
        assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': 'foo'}], {})
        assert_raises_rpc_error(-8, "Invalid parameter, vout must be positive", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': -1}], {})
        assert_raises_rpc_error(-8, "Invalid parameter, sequence number is out of range", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': 0, 'sequence': -1}], {})

        # Test `createrawtransaction` invalid `outputs`
        address = self.nodes[0].getnewaddress()
        address2 = self.nodes[0].getnewaddress()
        assert_raises_rpc_error(-1, "JSON value is not an array as expected", self.nodes[0].createrawtransaction, [], 'foo')
        self.nodes[0].createrawtransaction(inputs=[], outputs={})  # Should not throw for backwards compatibility
        self.nodes[0].createrawtransaction(inputs=[], outputs=[])
        assert_raises_rpc_error(-8, "Data must be hexadecimal string", self.nodes[0].createrawtransaction, [], {'data': 'foo'})
        assert_raises_rpc_error(-5, "Invalid Bitcoin address", self.nodes[0].createrawtransaction, [], {'foo': 0})
        assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].createrawtransaction, [], {address: 'foo'})
        assert_raises_rpc_error(-3, "Amount out of range", self.nodes[0].createrawtransaction, [], {address: -1})
        assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: %s" % address, self.nodes[0].createrawtransaction, [], multidict([(address, 1), (address, 1)]))
        assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: %s" % address, self.nodes[0].createrawtransaction, [], [{address: 1}, {address: 1}])
        assert_raises_rpc_error(-8, "Invalid parameter, duplicate key: data", self.nodes[0].createrawtransaction, [], [{"data": 'aa'}, {"data": "bb"}])
        assert_raises_rpc_error(-8, "Invalid parameter, duplicate key: data", self.nodes[0].createrawtransaction, [], multidict([("data", 'aa'), ("data", "bb")]))
        assert_raises_rpc_error(-8, "Invalid parameter, key-value pair must contain exactly one key", self.nodes[0].createrawtransaction, [], [{'a': 1, 'b': 2}])
        assert_raises_rpc_error(-8, "Invalid parameter, key-value pair not an object as expected", self.nodes[0].createrawtransaction, [], [['key-value pair1'], ['2']])

        # Test `createrawtransaction` invalid `locktime`
        assert_raises_rpc_error(-3, "Expected type number", self.nodes[0].createrawtransaction, [], {}, 'foo')
        assert_raises_rpc_error(-8, "Invalid parameter, locktime out of range", self.nodes[0].createrawtransaction, [], {}, -1)
        assert_raises_rpc_error(-8, "Invalid parameter, locktime out of range", self.nodes[0].createrawtransaction, [], {}, 4294967296)

        # Test `createrawtransaction` invalid `replaceable`
        assert_raises_rpc_error(-3, "Expected type bool", self.nodes[0].createrawtransaction, [], {}, 0, 'foo')

        self.log.info('Check that createrawtransaction accepts an array and object as outputs')
        tx = CTransaction()
        # One output
        tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs={address: 99}))))
        assert_equal(len(tx.vout), 1)
        assert_equal(
            tx.serialize().hex(),
            self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}]),
        )
        # Two outputs
        tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=OrderedDict([(address, 99), (address2, 99)])))))
        assert_equal(len(tx.vout), 2)
        assert_equal(
            tx.serialize().hex(),
            self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}, {address2: 99}]),
        )
        # Multiple mixed outputs
        tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=multidict([(address, 99), (address2, 99), ('data', '99')])))))
        assert_equal(len(tx.vout), 3)
        assert_equal(
            tx.serialize().hex(),
            self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}, {address2: 99}, {'data': '99'}]),
        )

        for type in ["bech32", "p2sh-segwit", "legacy"]:
            addr = self.nodes[0].getnewaddress("", type)
            addrinfo = self.nodes[0].getaddressinfo(addr)
            pubkey = addrinfo["scriptPubKey"]

            self.log.info('sendrawtransaction with missing prevtx info (%s)' %(type))

            # Test `signrawtransactionwithwallet` invalid `prevtxs`
            inputs  = [ {'txid' : txid, 'vout' : 3, 'sequence' : 1000}]
            outputs = { self.nodes[0].getnewaddress() : 1 }
            rawtx   = self.nodes[0].createrawtransaction(inputs, outputs)

            prevtx = dict(txid=txid, scriptPubKey=pubkey, vout=3, amount=1)
            succ = self.nodes[0].signrawtransactionwithwallet(rawtx, [prevtx])
            assert succ["complete"]
            if type == "legacy":
                del prevtx["amount"]
                succ = self.nodes[0].signrawtransactionwithwallet(rawtx, [prevtx])
                assert succ["complete"]

            if type != "legacy":
                assert_raises_rpc_error(-3, "Missing amount", self.nodes[0].signrawtransactionwithwallet, rawtx, [
                    {
                        "txid": txid,
                        "scriptPubKey": pubkey,
                        "vout": 3,
                    }
                ])

            assert_raises_rpc_error(-3, "Missing vout", self.nodes[0].signrawtransactionwithwallet, rawtx, [
                {
                    "txid": txid,
                    "scriptPubKey": pubkey,
                    "amount": 1,
                }
            ])
            assert_raises_rpc_error(-3, "Missing txid", self.nodes[0].signrawtransactionwithwallet, rawtx, [
                {
                    "scriptPubKey": pubkey,
                    "vout": 3,
                    "amount": 1,
                }
            ])
            assert_raises_rpc_error(-3, "Missing scriptPubKey", self.nodes[0].signrawtransactionwithwallet, rawtx, [
                {
                    "txid": txid,
                    "vout": 3,
                    "amount": 1
                }
            ])

        #########################################
        # sendrawtransaction with missing input #
        #########################################

        self.log.info('sendrawtransaction with missing input')
        inputs  = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1}] #won't exists
        outputs = { self.nodes[0].getnewaddress() : 4.998 }
        rawtx   = self.nodes[2].createrawtransaction(inputs, outputs)
        rawtx   = self.nodes[2].signrawtransactionwithwallet(rawtx)

        # This will raise an exception since there are missing inputs
        assert_raises_rpc_error(-25, "Missing inputs", self.nodes[2].sendrawtransaction, rawtx['hex'])

        #####################################
        # getrawtransaction with block hash #
        #####################################

        # make a tx by sending then generate 2 blocks; block1 has the tx in it
        tx = self.nodes[2].sendtoaddress(self.nodes[1].getnewaddress(), 1)
        block1, block2 = self.nodes[2].generate(2)
        self.sync_all()
        # We should be able to get the raw transaction by providing the correct block
        gottx = self.nodes[0].getrawtransaction(tx, True, block1)
        assert_equal(gottx['txid'], tx)
        assert_equal(gottx['in_active_chain'], True)
        # We should not have the 'in_active_chain' flag when we don't provide a block
        gottx = self.nodes[0].getrawtransaction(tx, True)
        assert_equal(gottx['txid'], tx)
        assert 'in_active_chain' not in gottx
        # We should not get the tx if we provide an unrelated block
        assert_raises_rpc_error(-5, "No such transaction found", self.nodes[0].getrawtransaction, tx, True, block2)
        # An invalid block hash should raise the correct errors
        assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[0].getrawtransaction, tx, True, True)
        assert_raises_rpc_error(-8, "parameter 3 must be of length 64 (not 6, for 'foobar')", self.nodes[0].getrawtransaction, tx, True, "foobar")
        assert_raises_rpc_error(-8, "parameter 3 must be of length 64 (not 8, for 'abcd1234')", self.nodes[0].getrawtransaction, tx, True, "abcd1234")
        assert_raises_rpc_error(-8, "parameter 3 must be hexadecimal string (not 'ZZZ0000000000000000000000000000000000000000000000000000000000000')", self.nodes[0].getrawtransaction, tx, True, "ZZZ0000000000000000000000000000000000000000000000000000000000000")
        assert_raises_rpc_error(-5, "Block hash not found", self.nodes[0].getrawtransaction, tx, True, "0000000000000000000000000000000000000000000000000000000000000000")
        # Undo the blocks and check in_active_chain
        self.nodes[0].invalidateblock(block1)
        gottx = self.nodes[0].getrawtransaction(txid=tx, verbose=True, blockhash=block1)
        assert_equal(gottx['in_active_chain'], False)
        self.nodes[0].reconsiderblock(block1)
        assert_equal(self.nodes[0].getbestblockhash(), block2)

        #########################
        # RAW TX MULTISIG TESTS #
        #########################
        # 2of2 test
        addr1 = self.nodes[2].getnewaddress()
        addr2 = self.nodes[2].getnewaddress()

        addr1Obj = self.nodes[2].getaddressinfo(addr1)
        addr2Obj = self.nodes[2].getaddressinfo(addr2)

        # Tests for createmultisig and addmultisigaddress
        assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 1, ["01020304"])
        self.nodes[0].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) # createmultisig can only take public keys
        assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1]) # addmultisigaddress can take both pubkeys and addresses so long as they are in the wallet, which is tested here.

        mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr1])['address']

        #use balance deltas instead of absolute values
        bal = self.nodes[2].getbalance()

        # send 1.2 BTC to msig adr
        txId = self.nodes[0].sendtoaddress(mSigObj, 1.2)
        self.sync_all()
        self.nodes[0].generate(1)
        self.sync_all()
        assert_equal(self.nodes[2].getbalance(), bal+Decimal('1.20000000')) #node2 has both keys of the 2of2 ms addr., tx should affect the balance

        # 2of3 test from different nodes
        bal = self.nodes[2].getbalance()
        addr1 = self.nodes[1].getnewaddress()
        addr2 = self.nodes[2].getnewaddress()
        addr3 = self.nodes[2].getnewaddress()

        addr1Obj = self.nodes[1].getaddressinfo(addr1)
        addr2Obj = self.nodes[2].getaddressinfo(addr2)
        addr3Obj = self.nodes[2].getaddressinfo(addr3)

        mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey'], addr3Obj['pubkey']])['address']

        txId = self.nodes[0].sendtoaddress(mSigObj, 2.2)
        decTx = self.nodes[0].gettransaction(txId)
        rawTx = self.nodes[0].decoderawtransaction(decTx['hex'])
        self.sync_all()
        self.nodes[0].generate(1)
        self.sync_all()

        #THIS IS AN INCOMPLETE FEATURE
        #NODE2 HAS TWO OF THREE KEY AND THE FUNDS SHOULD BE SPENDABLE AND COUNT AT BALANCE CALCULATION
        assert_equal(self.nodes[2].getbalance(), bal) #for now, assume the funds of a 2of3 multisig tx are not marked as spendable

        txDetails = self.nodes[0].gettransaction(txId, True)
        rawTx = self.nodes[0].decoderawtransaction(txDetails['hex'])
        vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('2.20000000'))

        bal = self.nodes[0].getbalance()
        inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "amount" : vout['value']}]
        outputs = { self.nodes[0].getnewaddress() : 2.19 }
        rawTx = self.nodes[2].createrawtransaction(inputs, outputs)
        rawTxPartialSigned = self.nodes[1].signrawtransactionwithwallet(rawTx, inputs)
        assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx

        rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx, inputs)
        assert_equal(rawTxSigned['complete'], True) #node2 can sign the tx compl., own two of three keys
        self.nodes[2].sendrawtransaction(rawTxSigned['hex'])
        rawTx = self.nodes[0].decoderawtransaction(rawTxSigned['hex'])
        self.sync_all()
        self.nodes[0].generate(1)
        self.sync_all()
        assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx

        # 2of2 test for combining transactions
        bal = self.nodes[2].getbalance()
        addr1 = self.nodes[1].getnewaddress()
        addr2 = self.nodes[2].getnewaddress()

        addr1Obj = self.nodes[1].getaddressinfo(addr1)
        addr2Obj = self.nodes[2].getaddressinfo(addr2)

        self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address']
        mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address']
        mSigObjValid = self.nodes[2].getaddressinfo(mSigObj)

        txId = self.nodes[0].sendtoaddress(mSigObj, 2.2)
        decTx = self.nodes[0].gettransaction(txId)
        rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex'])
        self.sync_all()
        self.nodes[0].generate(1)
        self.sync_all()

        assert_equal(self.nodes[2].getbalance(), bal) # the funds of a 2of2 multisig tx should not be marked as spendable

        txDetails = self.nodes[0].gettransaction(txId, True)
        rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex'])
        vout = next(o for o in rawTx2['vout'] if o['value'] == Decimal('2.20000000'))

        bal = self.nodes[0].getbalance()
        inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex'], "amount" : vout['value']}]
        outputs = { self.nodes[0].getnewaddress() : 2.19 }
        rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs)
        rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs)
        self.log.debug(rawTxPartialSigned1)
        assert_equal(rawTxPartialSigned1['complete'], False) #node1 only has one key, can't comp. sign the tx

        rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs)
        self.log.debug(rawTxPartialSigned2)
        assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx
        rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']])
        self.log.debug(rawTxComb)
        self.nodes[2].sendrawtransaction(rawTxComb)


# -*- coding: utf-8 -*-
"""
 /***************************************************************************
   QGIS Web Processing Service Plugin
  -------------------------------------------------------------------
 Date                 : 09 November 2009
 Copyright            : (C) 2009 by Dr. Horst Duester
 email                : horst dot duester at kappasys dot ch

  ***************************************************************************
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
  *   it under the terms of the GNU General Public License as published by  *
  *   the Free Software Foundation; either version 2 of the License, or     *
  *   (at your option) any later version.                                   *
  *                                                                         *
  ***************************************************************************/
"""
from __future__ import print_function
from __future__ import absolute_import

from future import standard_library
standard_library.install_aliases()
from builtins import str
from builtins import range
from qgis.PyQt.QtWidgets import *
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtNetwork import *
from qgis.PyQt import QtXml
from qgis.core import *
from . import version
from .qgswpsgui import QgsWpsGui
from .qgswpsdescribeprocessgui import QgsWpsDescribeProcessGui
from .qgswpstools import QgsWpsTools
from .qgsnewhttpconnectionbasegui import QgsNewHttpConnectionBaseGui
from .wpslib.wpsserver import WpsServer
from .wpslib.processdescription import ProcessDescription
from .wpslib.processdescription import getFileExtension,isMimeTypeVector,isMimeTypeRaster,isMimeTypeText,isMimeTypeFile,isMimeTypePlaylist
from .wpslib.processdescription import getFileExtension,isMimeTypeVector,isMimeTypeRaster,isMimeTypeText,isMimeTypeFile
from .wpslib.processdescription import StringInput, TextInput, SelectionInput, VectorInput, MultipleVectorInput, RasterInput, MultipleRasterInput, FileInput, MultipleFileInput, ExtentInput, CrsInput, VectorOutput, RasterOutput, StringOutput
from .wpslib.executionrequest import ExecutionRequest
from .wpslib.executionrequest import createTmpGML
from .wpslib.executionresult import ExecutionResult
from urllib.parse import urlparse
from .wpslib.wpsservercookie import WpsServerCookie

from .streaming import Streaming

from . import resources_rc
import string
from .apicompat.sipv2.compat import pystring

DEBUG = False

from .Ui_QgsWpsDockWidget import Ui_QgsWpsDockWidget

class QgsWpsDockWidget(QDockWidget, Ui_QgsWpsDockWidget):
    """
    Class documentation goes here.
    """

    killed = pyqtSignal()
    bookmarksChanged = pyqtSignal()

    #import pydevd_pycharm
    #pydevd_pycharm.settrace('localhost', port=1234, stdoutToServer=True, stderrToServer=True, suspend=False)

    def __init__(self, iface):
        """
        Constructor
        """
        QDockWidget.__init__(self, iface.mainWindow())
        self.setupUi(self)
        self.iface = iface
        self.tools = QgsWpsTools(self.iface)
        self.dataStream = None # Used for streaming
        self.setWindowTitle('QGIS WPS-Client '+version())

        self.defaultServers = {'Kappasys WPS':'https://kappasys.ch/cgi-bin/pywps.cgi',
            '52 North':'http://geoprocessing.demo.52north.org:8080/wps/WebProcessingService'
            }

        flags = Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint  # QgisGui.ModalDialogFlags
        self.dlg = QgsWpsGui(self.iface.mainWindow(), flags)
        self.dlg.getDescription.connect(self.getDescription)
        self.dlg.newServer.connect(self.newServer)
        self.dlg.editServer.connect(self.editServer)
        self.dlg.deleteServer.connect(self.deleteServer)
        self.dlg.pushDefaultServer.clicked.connect(self.pushDefaultServer)
        self.dlg.requestDescribeProcess.connect(self.requestDescribeProcess)
#        self.dlg.bookmarksChanged.connect()"), self, SIGNAL("bookmarksChanged()"))

        self.killed.connect(self.stopStreaming)

    def getDescription(self, name, item):
        self.requestDescribeProcess(name, item.text(0))

    def requestDescribeProcess(self, serverName, processIdentifier):
        server = WpsServer.getServer(serverName)
        self.process = ProcessDescription(server, processIdentifier)
        self.process.describeProcessFinished.connect(self.createProcessGUI)
        self.process.requestDescribeProcess()

    def setStatus(self, status, done=0, total=0):
        complete = status == "aborted" or status == "finished" or status == "error"

        self.progressBar.setRange(done, total)
        if status == "upload" and done == total:
            status = "processing"
            done = total = 0

        self.btnConnect.setEnabled(complete)
        self.btnKill.setEnabled(not complete)
        if complete:
            self.progressBar.setRange(0, 100)
            self.progressBar.setValue(100)
        else:
            self.progressBar.setRange(0, total)
            self.progressBar.setValue(done)

        if status == 'upload':
            text = QApplication.translate("QgsWps", " upload data ...")
        elif status == 'processing':
            text = QApplication.translate("QgsWps", " is running ...")
        elif status == 'download':
            text = QApplication.translate("QgsWps", " download data ...")
        elif status == 'finished':
            text = QApplication.translate("QgsWps", " finished successfully")
        elif status == 'error':
            text = QApplication.translate("QgsWps", " terminated with errors!")
        elif status == 'aborted':
            text = QApplication.translate("QgsWps", " was aborted!")

        self.statusLabel.setText(pystring(self.processIdentifier+text))

    def on_btnConnect_clicked(self):
        self.dlg.initQgsWpsGui()
        self.statusLabel.setText("")
        self.progressBar.setRange(0, 100)
        self.progressBar.setValue(0)
        self.dlg.show()

    def createProcessGUI(self):
        """Create the GUI for a selected WPS process based on the DescribeProcess
           response document. Mandatory inputs are marked as red, default is black"""

        # Lists which store the inputs and meta information (format, occurs, ...)
        # This list is initialized every time the GUI is created
        self.complexInputComboBoxList = [] # complex input for single raster and vector maps
        self.complexInputListWidgetList = [] # complex input for multiple raster and vector maps
        self.complexInputTextBoxList = [] # complex inpt of type text/plain
        self.literalInputComboBoxList = [] # literal value list with selectable answers
        self.literalInputLineEditList = [] # literal value list with single text line input
        self.bboxInputLineEditList = [] # bbox value list with single text line input
        self.complexOutputComboBoxList = [] # list combo box
        self.inputDataTypeList = {}
        self.outputDataTypeList = {}

        flags = Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint  # QgisGui.ModalDialogFlags

        self.processUrl = self.process.processUrl
        self.processIdentifier = self.process.processIdentifier
        self.processName = self.process.processName

        # Create the layouts and the scroll area
        self.dlgProcess = QgsWpsDescribeProcessGui(self.dlg, flags)
        self.dlgProcessLayout = QGridLayout()
        # Two tabs, one for the process inputs and one for the documentation
        # TODO: add a tab for literal outputs
        self.dlgProcessTab = QTabWidget()
        self.dlgProcessTabFrame = QFrame()
        self.dlgProcessTabFrameLayout = QGridLayout()
        # The process description can be very long, so we make it scrollable
        self.dlgProcessScrollArea = QScrollArea(self.dlgProcessTab)

        self.dlgProcessScrollAreaWidget = QFrame()
        self.dlgProcessScrollAreaWidgetLayout = QGridLayout()

        # First part of the gui is a short overview about the process
        self.identifier = self.process.identifier
        title = self.process.title
        abstract = self.process.abstract
        self.addIntroduction(self.identifier, title)

        # If no Input Data  are requested
        if len(self.process.inputs)==0:
          qDebug('Warning: no inputs defined for process: {} ?'.format(self.identifier))
          self.defineProcess()
          return 0

        # Generate the input GUI buttons and widgets

        res = self.generateProcessInputsGUI()
        if res == 0:
           return 0

        # Generate the editable outpt widgets, you can set the output to none if it is not requested
        self.generateProcessOutputsGUI()

        self.dlgProcessScrollAreaWidgetLayout.setSpacing(10)
        self.dlgProcessScrollAreaWidget.setLayout(self.dlgProcessScrollAreaWidgetLayout)
        self.dlgProcessScrollArea.setWidget(self.dlgProcessScrollAreaWidget)
        self.dlgProcessScrollArea.setWidgetResizable(True)

        self.dlgProcessTabFrameLayout.addWidget(self.dlgProcessScrollArea)

        self.addOkCancelButtons(self.dlgProcess,  self.dlgProcessTabFrameLayout)

        self.dlgProcessTabFrame.setLayout(self.dlgProcessTabFrameLayout)
        self.dlgProcessTab.addTab(self.dlgProcessTabFrame, "Process")

        self.tools.addDocumentationTab(self.dlgProcessTab,  abstract)

        self.dlgProcessLayout.addWidget(self.dlgProcessTab)
        self.dlgProcess.setLayout(self.dlgProcessLayout)
        self.dlgProcess.setGeometry(QRect(190,100,800,600))
        self.dlgProcess.show()

    def generateProcessInputsGUI(self):
        """Generate the GUI for all Inputs defined in the process description XML file"""
        for input in self.process.inputs:
            inputType = type(input)
            inputIdentifier = input.identifier
            title = input.title
            minOccurs = input.minOccurs
            if inputType == VectorInput or inputType == MultipleVectorInput:
                complexDataFormat = input.dataFormat
                self.inputDataTypeList[inputIdentifier] = complexDataFormat
                layerNamesList = self.tools.getLayerNameList(0)
                if inputType == VectorInput:
                    self.complexInputComboBoxList.append(self.tools.addComplexInputComboBox(title, inputIdentifier, str(complexDataFormat), layerNamesList, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout))
                else:
                    self.complexInputListWidgetList.append(self.tools.addComplexInputListWidget(title, inputIdentifier, str(complexDataFormat), layerNamesList, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout))
            elif inputType == StringInput:
                dValue =  input.defaultValue
                self.literalInputLineEditList.append(self.tools.addLiteralLineEdit(title, inputIdentifier, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout, dValue))
            elif inputType == TextInput:
                complexDataFormat = input.dataFormat
                if isMimeTypePlaylist(complexDataFormat["MimeType"]) != None:
                    self.inputDataTypeList[inputIdentifier] = complexDataFormat
                    # Playlist (text) inputs
                    self.complexInputTextBoxList.append(self.tools.addComplexInputTextBox(title, inputIdentifier, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout, str(complexDataFormat)))
                else:
                    self.complexInputTextBoxList.append(self.tools.addComplexInputTextBox(title, inputIdentifier, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout))
            elif inputType == RasterInput or inputType == MultipleRasterInput:
                complexDataFormat = input.dataFormat
                self.inputDataTypeList[inputIdentifier] = complexDataFormat
                layerNamesList = self.tools.getLayerNameList(1)
                if inputType == RasterInput:
                    self.complexInputComboBoxList.append(self.tools.addComplexInputComboBox(title, inputIdentifier, str(complexDataFormat), layerNamesList, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout))
                else:
                    self.complexInputListWidgetList.append(self.tools.addComplexInputListWidget(title, inputIdentifier, str(complexDataFormat), layerNamesList, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout))
            elif inputType == SelectionInput:
                valList = input.valList
                self.literalInputComboBoxList.append(self.tools.addLiteralComboBox(title, inputIdentifier, valList, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout))
            elif inputType == ExtentInput:
                myExtent = self.iface.mapCanvas().extent().toString().replace(':', ',')
                self.bboxInputLineEditList.append(self.tools.addLiteralLineEdit(title+"(minx,miny,maxx,maxy)", inputIdentifier, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout, myExtent))
            elif inputType == CrsInput:
                crsListe = input.crsList
#                self.literalInputComboBoxList.append(self.tools.addLiteralComboBox("Supported CRS", inputIdentifier, crsListe, minOccurs,  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout))

        self.tools.addCheckBox(QCoreApplication.translate("QgsWps","Process selected objects only"), QCoreApplication.translate("QgsWps","Selected"),  self.dlgProcessScrollAreaWidget,  self.dlgProcessScrollAreaWidgetLayout)

  ##############################################################################

    def generateProcessOutputsGUI(self):
        """Generate the GUI for all complex ouputs defined in the process description XML file"""

        if len(self.process.outputs) < 1:
            return

        groupbox = QGroupBox(self.dlgProcessScrollAreaWidget)
        groupbox.setTitle("Complex output(s)")
        layout = QVBoxLayout()

        for output in self.process.outputs:
            outputType = type(output)
            outputIdentifier = output.identifier
            title = output.title
            if outputType == VectorOutput or outputType == RasterOutput:
                complexOutputFormat = output.dataFormat
                # Store the input formats
                self.outputDataTypeList[outputIdentifier] = complexOutputFormat

                widget, comboBox = self.tools.addComplexOutputComboBox(groupbox, outputIdentifier, title, str(complexOutputFormat),  self.processIdentifier)
                self.complexOutputComboBoxList.append(comboBox)
                layout.addWidget(widget)

        # Set the layout
        groupbox.setLayout(layout)
        # Add the outputs
        self.dlgProcessScrollAreaWidgetLayout.addWidget(groupbox)

  ##############################################################################

    def addIntroduction(self,  name, title):

      groupbox = QGroupBox(self.dlgProcessScrollAreaWidget)
      groupbox.setTitle(name)
      layout = QVBoxLayout()

      myLabel = QLabel(groupbox)
      myLabel.setObjectName("qLabel"+name)
      myLabel.setText(pystring(title))
      myLabel.setMinimumWidth(600)
      myLabel.setMinimumHeight(25)
      myLabel.setWordWrap(True)

      layout.addWidget(myLabel)

      groupbox.setLayout(layout)

      self.dlgProcessScrollAreaWidgetLayout.addWidget(groupbox)

  ##############################################################################

  ##############################################################################
    def defineProcess(self):
        """Create the execute request"""
        self.dlgProcess.close()
        self.dlg.close()
        doc = QtXml.QDomDocument()
        doc.setContent(self.process.processXML)

        QApplication.setOverrideCursor(Qt.WaitCursor)

        checkBoxes = self.dlgProcess.findChildren(QCheckBox)
        if len(checkBoxes) > 0:
          useSelected = checkBoxes[0].isChecked()

        request = ExecutionRequest(self.process)
        request.addExecuteRequestHeader()

        request.addDataInputsStart()
        if len(self.process.inputs) > 0:
            # text/plain inputs ########################################################
            for textBox in self.complexInputTextBoxList:
              # Do not add undefined inputs
              if textBox == None or str(textBox.document().toPlainText()) == "":
                continue

              # TODO: Check for more types (e.g. KML, Shapefile, JSON)
              self.mimeType = self.inputDataTypeList[textBox.objectName()]["MimeType"]

              if isMimeTypePlaylist(self.mimeType) != None:
                schema = self.inputDataTypeList[textBox.objectName()]["Schema"]
                encoding = self.inputDataTypeList[textBox.objectName()]["Encoding"]

                # Handle 'as reference' playlist
                request.addReferenceInput(textBox.objectName(), self.mimeType, schema, encoding, textBox.document().toPlainText())

              else: # It's not a playlist
                request.addPlainTextInput(textBox.objectName(), textBox.document().toPlainText())

            # Single raster and vector inputs ##########################################
            for comboBox in self.complexInputComboBoxList:
              # Do not add undefined inputs

              if comboBox == None or comboBox.currentText() == "<None>":
                continue

              # TODO: Check for more types (e.g. KML, Shapefile, JSON)
#              QMessageBox.information(None, '',  str(self.inputDataTypeList[comboBox.objectName()]["MimeType"]))
#              QMessageBox.information(None, '',  pystring(comboBox.objectName()))

              self.mimeType = self.inputDataTypeList[pystring(comboBox.objectName())]["MimeType"]
              schema = self.inputDataTypeList[pystring(comboBox.objectName())]["Schema"]
              encoding = self.inputDataTypeList[pystring(comboBox.objectName())]["Encoding"]
              self.myLayer = self.tools.getVLayer(comboBox.currentText())

#              try:
              if isMimeTypeVector(self.mimeType) != None and encoding != "base64":
                  gmldata = createTmpGML(self.tools.getVLayer(comboBox.currentText()),
                                                    useSelected, self.process.getSupportedGMLVersion(comboBox.objectName()))
                  request.addGeometryInput(comboBox.objectName(), self.mimeType, schema, encoding, gmldata, useSelected)
              elif isMimeTypeVector(self.mimeType) != None or isMimeTypeRaster(self.mimeType) != None:
                  request.addGeometryBase64Input(comboBox.objectName(), self.mimeType, self.tools.getVLayer(comboBox.currentText()))
#              except:
#                  QApplication.restoreOverrideCursor()
#                  QMessageBox.warning(self.iface.mainWindow(),
#                      QApplication.translate("QgsWps","Error"),
#                      QApplication.translate("QgsWps","Please load or select a vector layer!"))
#                  return

            # Multiple raster and vector inputs ########################################
            for listWidgets in self.complexInputListWidgetList:
              # Do not add undefined inputs
              if listWidgets == None:
                continue

              self.mimeType = self.inputDataTypeList[listWidgets.objectName()]["MimeType"]
              schema = self.inputDataTypeList[listWidgets.objectName()]["Schema"]
              encoding = self.inputDataTypeList[listWidgets.objectName()]["Encoding"]

              # Iterate over each seletced item
              for i in range(listWidgets.count()):
                listWidget = listWidgets.item(i)
                if listWidget == None or listWidget.isSelected() == False or str(listWidget.text()) == "<None>":
                  continue

                # TODO: Check for more types
                if isMimeTypeVector(self.mimeType) != None and self.mimeType == "text/xml":
                  gmldata = createTmpGML(self.tools.getVLayer(listWidget.text()),
                    useSelected, self.process.getSupportedGMLVersion(listWidgets.objectName()))
                  request.addMultipleGeometryInput(listWidgets.objectName(), self.mimeType, schema, encoding, gmldata, useSelected)
                elif isMimeTypeVector(self.mimeType) != None or isMimeTypeRaster(self.mimeType) != None:
                  request.addMultipleGeometryBase64Input(listWidgets.objectName(), self.mimeType, self.tools.getVLayer(listWidget.text()))

            # Literal data as combo box choice #########################################
            for comboBox in self.literalInputComboBoxList:
              if comboBox == None or comboBox.currentText() == "":
                  continue

              request.addLiteralDataInput(comboBox.objectName(), comboBox.currentText())

           # Literal data as combo box choice #########################################
            for lineEdit in self.literalInputLineEditList:
              if lineEdit == None or lineEdit.text() == "":
                  continue

              request.addLiteralDataInput(lineEdit.objectName(), lineEdit.text())

            # BBOX data as lineEdit #########################################
            for bbox in self.bboxInputLineEditList:
              if bbox == None or bbox.text() == "":
                  continue

              bboxArray = bbox.text().split(',')

              request.addBoundingBoxInput(bbox.objectName(), bboxArray)

        request.addDataInputsEnd()

        # Attach only defined outputs
        dataOutputs = doc.elementsByTagName("Output")
        if dataOutputs.size() > 0 or len(self.complexOutputComboBoxList) > 0:
          request.addResponseFormStart()

          # Attach ALL literal outputs #############################################
          for i in range(dataOutputs.size()):
            f_element = dataOutputs.at(i).toElement()
            outputIdentifier = pystring(f_element.elementsByTagName("ows:Identifier").at(0).toElement().text()).strip()
            literalOutputType = f_element.elementsByTagName("LiteralOutput")

            # Complex data is always requested as reference
            if literalOutputType.size() != 0:
              request.addLiteralDataOutput(outputIdentifier)

          # Attach selected complex outputs ########################################
          for comboBox in self.complexOutputComboBoxList:
            # Do not add undefined outputs
            if comboBox == None or str(comboBox.currentText()) == "<None>":
              continue
            outputIdentifier = comboBox.objectName()

            self.mimeType = self.outputDataTypeList[pystring(outputIdentifier)]["MimeType"]
            schema = self.outputDataTypeList[pystring(outputIdentifier)]["Schema"]
            encoding = self.outputDataTypeList[pystring(outputIdentifier)]["Encoding"]

            request.addReferenceOutput(outputIdentifier, self.mimeType, schema, encoding)

          request.addResponseFormEnd()

        request.addExecuteRequestEnd()
        postString = request.request

        # This is for debug purpose only


import logging
import os.path
import sys
import unittest
from collections import namedtuple

from configobj import ConfigObj

from sickchill import settings
from sickchill.oldbeard import config, scheduler

class ConfigTestBasic(unittest.TestCase):
    """
    Test basic methods in oldbeard.config
    """

    def test_check_section(self):
        """
        Test check_section
        """
        CFG = ConfigObj('config.ini', encoding='UTF-8', indent_type='  ')
        self.assertFalse(config.check_section(CFG, 'General'))
        self.assertTrue(config.check_section(CFG, 'General'))

    def test_checkbox_to_value(self):
        """
        Test checkbox_to_value
        """
        self.assertTrue(config.checkbox_to_value(1))
        self.assertTrue(config.checkbox_to_value(['option', 'True']))
        self.assertEqual(config.checkbox_to_value('0', 'yes', 'no'), 'no')

    def test_clean_host(self):
        """
        Test clean_host
        """
        self.assertEqual(config.clean_host('http://127.0.0.1:8080'), '127.0.0.1:8080')
        self.assertEqual(config.clean_host('https://mail.google.com/mail'), 'mail.google.com')
        self.assertEqual(config.clean_host('http://localhost:8081/home/displayShow?show=80379#season-10'),
                         'localhost:8081')
        self.assertEqual(config.clean_host('http://testme.co.uk', 9000), 'testme.co.uk:9000')  # default port
        self.assertEqual(config.clean_host('www.google.com/search'), 'www.google.com')
        self.assertEqual(config.clean_host(''), '') # empty host

    def test_clean_hosts(self):
        """
        Test clean_hosts
        """
        dirty_hosts = 'http://127.0.0.1:8080,https://mail.google.com/mail,' \
                      'http://localhost:8081/home/displayShow?show=80379#season-10,' \
                      'www.google.com/search,'
        clean_result = '127.0.0.1:8080,mail.google.com:5050,localhost:8081,www.google.com:5050'
        self.assertEqual(config.clean_hosts(dirty_hosts, '5050'), clean_result)

    def test_clean_url(self):
        """
        Test cleaning of urls
        """
        log = logging.getLogger(__name__)
        test = namedtuple('test', 'expected_result dirty clean')

        url_tests = [
            test(True, "https://subdomain.domain.tld/endpoint", "https://subdomain.domain.tld/endpoint"),  # does not add a final /
            test(True, "http://www.example.com/folder/", "http://www.example.com/folder/"),  # does not remove the final /
            test(True, "google.com/xml.rpc", "http://google.com/xml.rpc"),  # add scheme if missing
            test(True, "google.com", "http://google.com/"),  # add scheme if missing and final / if its just the domain
            test(True, "scgi:///home/user/.config/path/socket", "scgi:///home/user/.config/path/socket"),  # scgi identified as scheme
            test(True, None, ''),  # None URL returns empty string
            test(False, "https://subdomain.domain.tld/endpoint", "http://subdomain.domain.tld/endpoint"),  # does not change schemes from https to http
            test(False, "http://subdomain.domain.tld/endpoint", "https://subdomain.domain.tld/endpoint"),  # ...or vice versa
            test(False, "google.com/xml.rpc", "google.com/xml.rpc"),  # scheme is always added
            test(False, "google.com", "https://google.com/"),  # does not default to https
            test(False, "http://www.example.com/folder/", "http://www.example.com/folder"),  # does not strip final /
            test(False, "scgi:///home/user/.config/path/socket", "scgi:///home/user/.config/path/socket/"),  # does not add a final /
            test(AttributeError, 1, 1),  # None URL returns empty string
        ]

        for test_url in url_tests:
            if issubclass(type(Exception), type(test_url.expected_result)):
                with self.assertRaises(test_url.expected_result):
                    self.assertEqual(config.clean_url(test_url.dirty), test_url.clean)
            elif test_url.expected_result is True:
                self.assertEqual(config.clean_url(test_url.dirty), test_url.clean)
            elif test_url.expected_result is False:
                self.assertNotEqual(config.clean_url(test_url.dirty), test_url.clean)
            else:
                log.error('Test not defined for %s', test_url)

    def test_min_max(self):
        """
        Test min_max
        """

        self.assertEqual(config.min_max('100', default=50, low=50, high=200), 100)
        self.assertEqual(config.min_max('25', default=50, low=50, high=200), 50)
        self.assertEqual(config.min_max('250', default=50, low=50, high=200), 200)

    def test_check_setting_int(self):
        """
        Test check_setting_int
        """
        # setup
        CFG = ConfigObj('config.ini', encoding='UTF-8', indent_type='  ')
        config.check_section(CFG, 'General')
        CFG['General']['indexer_timeout'] = 60
        CFG['General']['use_icacls'] = 'True'
        CFG['General']['use_nzbs'] = 'False'
        CFG['General']['status_default'] = None
        # normal
        self.assertEqual(config.check_setting_int(CFG, 'General', 'indexer_timeout', 30), 60)
        self.assertEqual(CFG['General']['indexer_timeout'], 60)
        # force min/max
        self.assertEqual(config.check_setting_int(CFG, 'General', 'indexer_timeout', 150, 100, 200), 150)
        self.assertEqual(CFG['General']['indexer_timeout'], 150)
        self.assertEqual(config.check_setting_int(CFG, 'General', 'indexer_timeout', 250, 200, 300, False), 200)
        self.assertEqual(CFG['General']['indexer_timeout'], 200)
        self.assertEqual(config.check_setting_int(CFG, 'General', 'indexer_timeout', 90, 50, 100), 90)
        self.assertEqual(CFG['General']['indexer_timeout'], 90)
        self.assertEqual(config.check_setting_int(CFG, 'General', 'indexer_timeout', 20, 10, 30, False), 30)
        self.assertEqual(CFG['General']['indexer_timeout'], 30)
        # true/false => int
        self.assertEqual(config.check_setting_int(CFG, 'General', 'use_icacls', 1), 1)
        self.assertEqual(CFG['General']['use_icacls'], 'True')
        self.assertEqual(config.check_setting_int(CFG, 'General', 'use_nzbs', 0), 0)
        self.assertEqual(CFG['General']['use_nzbs'], 'False')
        # None value type + silent off
        self.assertEqual(config.check_setting_int(CFG, 'General', 'status_default', 5, silent=False), 5)
        self.assertEqual(CFG['General']['status_default'], 5)
        # unmatched section
        self.assertEqual(config.check_setting_int(CFG, 'Subtitles', 'subtitles_finder_frequency', 1), 1)
        self.assertEqual(CFG['Subtitles']['subtitles_finder_frequency'], 1)
        # wrong def_val/min/max type
        self.assertEqual(config.check_setting_int(CFG, 'General', 'indexer_timeout', 'ba', 'min', 'max'), 30)
        self.assertEqual(CFG['General']['indexer_timeout'], 30)

    def test_check_setting_float(self):
        """
        Test check_setting_float
        """
        # setup
        CFG = ConfigObj('config.ini', encoding='UTF-8', indent_type='  ')
        config.check_section(CFG, 'General')
        CFG['General']['fanart_background_opacity'] = 0.5
        CFG['General']['log_size'] = None
        # normal
        self.assertEqual(config.check_setting_float(CFG, 'General', 'fanart_background_opacity', 0.4), 0.5)
        self.assertEqual(CFG['General']['fanart_background_opacity'], 0.5)
        # force min/max
        self.assertEqual(config.check_setting_float(CFG, 'General', 'fanart_background_opacity', 0.7, 0.6, 1.0), 0.7)
        self.assertEqual(CFG['General']['fanart_background_opacity'], 0.7)
        self.assertEqual(config.check_setting_float(CFG, 'General', 'fanart_background_opacity', 0.7, 0.8, 1.0, False), 0.8)
        self.assertEqual(CFG['General']['fanart_background_opacity'], 0.8)
        self.assertEqual(config.check_setting_float(CFG, 'General', 'fanart_background_opacity', 0.3, 0.1, 0.4), 0.3)
        self.assertEqual(CFG['General']['fanart_background_opacity'], 0.3)
        self.assertEqual(config.check_setting_float(CFG, 'General', 'fanart_background_opacity', 0.1, 0.1, 0.2, False), 0.2)
        self.assertEqual(CFG['General']['fanart_background_opacity'], 0.2)
        # None value type + silent off
        self.assertEqual(config.check_setting_float(CFG, 'General', 'log_size', 10.0, silent=False), 10.0)
        self.assertEqual(CFG['General']['log_size'], 10.0)
        # unmatched section
        self.assertEqual(config.check_setting_float(CFG, 'Kodi', 'log_size', 2.5), 2.5)
        self.assertEqual(CFG['Kodi']['log_size'], 2.5)
        # wrong def_val/min/max type
        self.assertEqual(config.check_setting_float(CFG, 'General', 'fanart_background_opacity', 'ba', 'min', 'max'), 0.2)
        self.assertEqual(CFG['General']['fanart_background_opacity'], 0.2)

    def test_check_setting_str(self):
        """
        Test check_setting_str
        """
        # setup
        CFG = ConfigObj('config.ini', encoding='UTF-8', indent_type='  ')
        config.check_section(CFG, 'General')
        CFG['General']['process_method'] = "copy"
        CFG['General']['git_password'] = "SFa342FHb_"
        CFG['General']['extra_scripts'] = None
        # normal
        self.assertEqual(config.check_setting_str(CFG, 'General', 'process_method', 'move'), 'copy')
        self.assertEqual(config.check_setting_str(CFG, 'General', 'git_password', '', silent=False, censor_log=True),
                         'SFa342FHb_')
        # None value type
        self.assertEqual(config.check_setting_str(CFG, 'General', 'extra_scripts', ''), '')
        # unmatched section
        self.assertEqual(config.check_setting_str(CFG, 'Subtitles', 'subtitles_languages', 'eng'), 'eng')
        # wrong def_val type
        self.assertEqual(config.check_setting_str(CFG, 'General', 'process_method', ['fail']), 'copy')

    def test_check_setting_bool(self):
        """
        Test check_setting_bool
        """
        # setup
        CFG = ConfigObj('config.ini', encoding='UTF-8', indent_type='  ')
        config.check_section(CFG, 'General')
        CFG['General']['debug'] = True
        CFG['General']['season_folders_default'] = False
        CFG['General']['dbdebug'] = None
        # normal
        self.assertTrue(config.check_setting_bool(CFG, 'General', 'debug'))
        self.assertFalse(config.check_setting_bool(CFG, 'General', 'season_folders_default', def_val=True))
        # None value type
        self.assertFalse(config.check_setting_bool(
            CFG, 'General', 'dbdebug', False))
        # unmatched item
        self.assertTrue(config.check_setting_bool(CFG, 'General', 'git_reset', def_val=True))
        # unmatched section
        self.assertFalse(config.check_setting_bool(CFG, 'Subtitles', 'use_subtitles', def_val=False))
        # wrong def_val type, silent = off
        self.assertTrue(config.check_setting_bool(
            CFG, 'General', 'debug', def_val=['fail'], silent=False))

class ConfigTestChanges(unittest.TestCase):
    """
    Test change methods in oldbeard.config
    """

    def test_change_https_cert(self):
        """
        Test change_https_cert
        """
        settings.HTTPS_CERT = 'server.crt' # Initialize
        self.assertTrue(config.change_https_cert(''))
        self.assertTrue(config.change_https_cert('server.crt'))
        self.assertFalse(config.change_https_cert('/:/server.crt')) # INVALID
        settings.HTTPS_CERT = ''

    def test_change_https_key(self):
        """
        Test change_https_key
        """
        settings.HTTPS_KEY = 'server.key'  # Initialize
        self.assertTrue(config.change_https_key(''))
        self.assertTrue(config.change_https_key('server.key'))
        self.assertFalse(config.change_https_key('/:/server.key')) # INVALID
        settings.HTTPS_KEY = ''

    def test_change_sickchill_background(self):
        """
        Test change_sickchill_background
        """
        settings.SICKCHILL_BACKGROUND_PATH = ''  # Initialize
        self.assertTrue(config.change_sickchill_background(__file__))
        self.assertFalse(config.change_sickchill_background('not_real.jpg'))
        self.assertTrue(config.change_sickchill_background(''))

    def test_change_custom_css(self):
        """
        Test change_custom_css
        """
        settings.CUSTOM_CSS_PATH = ''  # Initialize
        self.assertFalse(config.change_custom_css(__file__)) # not a css file
        self.assertFalse(config.change_custom_css('not_real.jpg')) # doesn't exist
        self.assertFalse(config.change_custom_css('sickchill_tests')) # isn't a file
        css_file = os.path.join(os.path.dirname(__file__), 'custom.css')
        with open(css_file, 'w') as f:
            f.write('table.main {\n    width: 100%;\n}')
        self.assertTrue(config.change_custom_css(css_file)) # real
        os.remove(css_file)
        self.assertTrue(config.change_custom_css('')) # empty

    def test_change_nzb_dir(self):
        """
        Test change_nzb_dir
        """
        settings.NZB_DIR = ''
        self.assertTrue(config.change_nzb_dir('cache'))
        self.assertFalse(config.change_nzb_dir('/:/NZB_Downloads')) # INVALID
        self.assertTrue(config.change_nzb_dir(''))

    def test_change_torrent_dir(self):
        """
        Test change_torrent_dir
        """
        settings.TORRENT_DIR = ''
        self.assertTrue(config.change_torrent_dir('cache'))
        self.assertFalse(config.change_torrent_dir('/:/Downloads')) # INVALID
        self.assertTrue(config.change_torrent_dir(''))

    def test_change_tv_download_dir(self):
        """
        Test change_tv_download_dir
        """
        settings.TV_DOWNLOAD_DIR = ''
        self.assertTrue(config.change_tv_download_dir('cache'))
        self.assertFalse(config.change_tv_download_dir('/:/Downloads/Completed')) # INVALID

        self.assertTrue(config.change_tv_download_dir(''))
        self.assertEqual(settings.TV_DOWNLOAD_DIR, '')

    def test_change_unpack_dir(self):
        """
        Test change_unpack_dir
        """
        settings.UNPACK_DIR = ''
        self.assertTrue(config.change_unpack_dir('cache'))
        self.assertFalse(config.change_unpack_dir('/:/Extract')) # INVALID
        self.assertTrue(config.change_unpack_dir(''))

    def test_change_auto_pp_freq(self):
        """
        Test change_postprocessor_frequency
        """
        settings.autoPostProcessorScheduler = scheduler.Scheduler(lambda:None) # dummy

        config.change_postprocessor_frequency(0)
        self.assertEqual(settings.AUTOPOSTPROCESSOR_FREQUENCY, settings.MIN_AUTOPOSTPROCESSOR_FREQUENCY)
        config.change_postprocessor_frequency('s')
        self.assertEqual(settings.AUTOPOSTPROCESSOR_FREQUENCY, settings.DEFAULT_AUTOPOSTPROCESSOR_FREQUENCY)
        config.change_postprocessor_frequency(60)
        self.assertEqual(settings.AUTOPOSTPROCESSOR_FREQUENCY, 60)

    def test_change_daily_search_freq(self):
        """
        Test change_daily_search_frequency
        """
        settings.dailySearchScheduler = scheduler.Scheduler(lambda:None) # dummy

        config.change_daily_search_frequency(0)
        self.assertEqual(settings.DAILYSEARCH_FREQUENCY, settings.MIN_DAILYSEARCH_FREQUENCY)
        config.change_daily_search_frequency('s')
        self.assertEqual(settings.DAILYSEARCH_FREQUENCY, settings.DEFAULT_DAILYSEARCH_FREQUENCY)
        config.change_daily_search_frequency(60)
        self.assertEqual(settings.DAILYSEARCH_FREQUENCY, 60)

    def test_change_backlog_freq(self):
        """
        Test change_backlog_frequency
        """
        settings.backlogSearchScheduler = scheduler.Scheduler(lambda:None) # dummy
        settings.DAILYSEARCH_FREQUENCY = settings.DEFAULT_DAILYSEARCH_FREQUENCY  # needed

        config.change_backlog_frequency(0)
        self.assertEqual(settings.BACKLOG_FREQUENCY, settings.MIN_BACKLOG_FREQUENCY)
        config.change_backlog_frequency('s')
        self.assertEqual(settings.BACKLOG_FREQUENCY, settings.MIN_BACKLOG_FREQUENCY)
        config.change_backlog_frequency(1440)
        self.assertEqual(settings.BACKLOG_FREQUENCY, 1440)

    def test_change_update_freq(self):
        """
        Test change_update_frequency
        """
        settings.versionCheckScheduler = scheduler.Scheduler(lambda:None) # dummy

        config.change_update_frequency(0)
        self.assertEqual(settings.UPDATE_FREQUENCY, settings.MIN_UPDATE_FREQUENCY)
        config.change_update_frequency('s')
        self.assertEqual(settings.UPDATE_FREQUENCY, settings.DEFAULT_UPDATE_FREQUENCY)
        config.change_update_frequency(60)
        self.assertEqual(settings.UPDATE_FREQUENCY, 60)

    def test_change_show_update_hour(self):
        """
        Test change_showupdate_hour
        """
        settings.showUpdateScheduler = scheduler.Scheduler(lambda:None) # dummy

        config.change_showupdate_hour(-2)
        self.assertEqual(settings.SHOWUPDATE_HOUR, 0)
        config.change_showupdate_hour('s')
        self.assertEqual(settings.SHOWUPDATE_HOUR, settings.DEFAULT_SHOWUPDATE_HOUR)
        config.change_showupdate_hour(60)
        self.assertEqual(settings.SHOWUPDATE_HOUR, 0)
        config.change_showupdate_hour(12)
        self.assertEqual(settings.SHOWUPDATE_HOUR, 12)

    def test_change_sub_finder_freq(self):
        """
        Test change_subtitle_finder_frequency
        """
        config.change_subtitle_finder_frequency('')
        self.assertEqual(settings.SUBTITLES_FINDER_FREQUENCY, 1)
        config.change_subtitle_finder_frequency('s')
        self.assertEqual(settings.SUBTITLES_FINDER_FREQUENCY, 1)
        config.change_subtitle_finder_frequency(8)
        self.assertEqual(settings.SUBTITLES_FINDER_FREQUENCY, 8)

    def test_change_version_notify(self):
        """
        Test change_version_notify
        """
        class dummy_action(object): # needed for *scheduler.action.forceRun()
            def __init__(self):
                self.amActive = False

        settings.versionCheckScheduler = scheduler.Scheduler(dummy_action()) # dummy
        settings.VERSION_NOTIFY = True

        config.change_version_notify(True) # no change
        self.assertTrue(settings.VERSION_NOTIFY)
        config.change_version_notify('stop') # = defaults to False
        self.assertFalse(settings.VERSION_NOTIFY and settings.versionCheckScheduler.enable)
        config.change_version_notify('on')
        self.assertTrue(settings.VERSION_NOTIFY and settings.versionCheckScheduler.enable)

    def test_change_download_propers(self):
        """
        Test change_download_propers
        """
        settings.properFinderScheduler = scheduler.Scheduler(lambda:None) # dummy
        settings.DOWNLOAD_PROPERS = True

        config.change_download_propers(True) # no change
        self.assertTrue(settings.DOWNLOAD_PROPERS)
        config.change_download_propers('stop') # = defaults to False
        self.assertFalse(settings.DOWNLOAD_PROPERS and settings.properFinderScheduler.enable)
        config.change_download_propers('on')
        self.assertTrue(settings.DOWNLOAD_PROPERS and settings.properFinderScheduler.enable)

    def test_change_use_trakt(self):
        """
        Test change_use_trakt
        """
        settings.traktCheckerScheduler = scheduler.Scheduler(lambda:None) # dummy
        settings.USE_TRAKT = True

        config.change_use_trakt(True) # no change
        self.assertTrue(settings.USE_TRAKT)
        config.change_use_trakt('stop') # = defaults to False
        self.assertFalse(settings.USE_TRAKT and settings.traktCheckerScheduler.enable)
        config.change_use_trakt('on')
        self.assertTrue(settings.USE_TRAKT and settings.traktCheckerScheduler.enable)

    def test_change_use_subtitles(self):
        """
        Test change_use_subtitles
        """
        settings.subtitlesFinderScheduler = scheduler.Scheduler(lambda:None) # dummy
        settings.USE_SUBTITLES = True

        config.change_use_subtitles(True) # no change
        self.assertTrue(settings.USE_SUBTITLES)
        config.change_use_subtitles('stop') # = defaults to False
        self.assertFalse(settings.USE_SUBTITLES and settings.subtitlesFinderScheduler.enable)
        config.change_use_subtitles('on')
        self.assertTrue(settings.USE_SUBTITLES and settings.subtitlesFinderScheduler.enable)

    def test_change_process_auto(self):
        """
        Test change_process_automatically
        """
        settings.autoPostProcessorScheduler = scheduler.Scheduler(lambda:None) # dummy
        settings.PROCESS_AUTOMATICALLY = True

        config.change_process_automatically(True) # no change
        self.assertTrue(settings.PROCESS_AUTOMATICALLY)
        config.change_process_automatically('stop') # = defaults to False
        self.assertFalse(settings.PROCESS_AUTOMATICALLY and settings.autoPostProcessorScheduler.enable)
        config.change_process_automatically('on')
        self.assertTrue(settings.PROCESS_AUTOMATICALLY and settings.autoPostProcessorScheduler.enable)

class ConfigTestMigrator(unittest.TestCase):
    """
    Test the oldbeard.config.ConfigMigrator class
    """
    @unittest.expectedFailure  # Not fully implemented
    def test_config_migrator(self):
        """
        Test migrate_config
        """
        # TODO: Assert the 'too-advanced-config-version' error

        CFG = ConfigObj('config.ini', encoding='UTF-8', indent_type='  ')
        config.check_section(CFG, 'General')
        CFG['General']['config_version'] = 0
        settings.CONFIG_VERSION = 13
        settings.CONFIG_FILE = 'config.ini'


#!/usr/bin/python
# Copyright: Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type

ANSIBLE_METADATA = {'status': ['preview'],
                    'supported_by': 'community',
                    'metadata_version': '1.0'}

DOCUMENTATION = '''
---
module: ecs_attribute
short_description: manage ecs attributes
description:
    - Create, update or delete ECS container instance attributes.
version_added: "2.4"
author: Andrej Svenke (@anryko)
requirements: [ botocore, boto3 ]
options:
    cluster:
        description:
            - The short name or full Amazon Resource Name (ARN) of the cluster
              that contains the resource to apply attributes.
        required: true
    state:
        description:
            - The desired state of the attributes.
        required: false
        default: present
        choices: ['present', 'absent']
    attributes:
        description:
            - List of attributes.
        required: true
        suboptions:
            name:
                description:
                    - The name of the attribute. Up to 128 letters (uppercase and lowercase),
                      numbers, hyphens, underscores, and periods are allowed.
                required: true
            value:
                description:
                    - The value of the attribute. Up to 128 letters (uppercase and lowercase),
                      numbers, hyphens, underscores, periods, at signs (@), forward slashes, colons,
                      and spaces are allowed.
                required: false
    ec2_instance_id:
        description:
            - EC2 instance ID of ECS cluster container instance.
        required: true
extends_documentation_fragment:
    - aws
    - ec2
'''

EXAMPLES = '''
# Note: These examples do not set authentication details, see the AWS Guide for details.

# Set attributes
- ecs_attribute:
    state: present
    cluster: test-cluster
    ec2_instance_id: "{{ ec2_id }}"
    attributes:
      - flavor: test
      - migrated
  delegate_to: localhost

# Delete attributes
- ecs_attribute:
    state: absent
    cluster: test-cluster
    ec2_instance_id: "{{ ec2_id }}"
    attributes:
      - flavor: test
      - migrated
  delegate_to: localhost
'''

RETURN = '''
attributes:
    description: attributes
    type: complex
    returned: always
    contains:
        cluster:
            description: cluster name
            type: string
        ec2_instance_id:
            description: ec2 instance id of ecs container instance
            type: string
        attributes:
            description: list of attributes
            type: list of complex
            contains:
                name:
                    description: name of the attribute
                    type: string
                value:
                    description: value of the attribute
                    returned: if present
                    type: string
'''

try:
    import boto3
    from botocore.exceptions import ClientError, EndpointConnectionError
    HAS_BOTO3 = True
except ImportError:
    HAS_BOTO3 = False

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info

class EcsAttributes(object):
    """Handles ECS Cluster Attribute"""

    def __init__(self, module, attributes):
        self.module = module
        self.attributes = attributes if self._validate_attrs(attributes) else self._parse_attrs(attributes)

    def __bool__(self):
        return bool(self.attributes)

    __nonzero__ = __bool__

    def __iter__(self):
        return iter(self.attributes)

    @staticmethod
    def _validate_attrs(attrs):
        return all(tuple(attr.keys()) in (('name', 'value'), ('value', 'name')) for attr in attrs)

    def _parse_attrs(self, attrs):
        attrs_parsed = []
        for attr in attrs:
            if isinstance(attr, dict):
                if len(attr) != 1:
                    self.module.fail_json(msg="Incorrect attribute format - %s" % str(attr))
                name, value = list(attr.items())[0]
                attrs_parsed.append({'name': name, 'value': value})
            elif isinstance(attr, str):
                attrs_parsed.append({'name': attr, 'value': None})
            else:
                self.module.fail_json(msg="Incorrect attributes format - %s" % str(attrs))

        return attrs_parsed

    def _setup_attr_obj(self, ecs_arn, name, value=None, skip_value=False):
        attr_obj = {'targetType': 'container-instance',
                    'targetId': ecs_arn,
                    'name': name}
        if not skip_value and value is not None:
            attr_obj['value'] = value

        return attr_obj

    def get_for_ecs_arn(self, ecs_arn, skip_value=False):
        """
        Returns list of attribute dicts ready to be passed to boto3
        attributes put/delete methods.
        """
        return [self._setup_attr_obj(ecs_arn, skip_value=skip_value, **attr) for attr in self.attributes]

    def diff(self, attrs):
        """
        Returns EcsAttributes Object containing attributes which are present
        in self but are absent in passed attrs (EcsAttributes Object).
        """
        attrs_diff = [attr for attr in self.attributes if attr not in attrs]
        return EcsAttributes(self.module, attrs_diff)

class Ec2EcsInstance(object):
    """Handle ECS Cluster Remote Operations"""

    def __init__(self, module, cluster, ec2_id):
        self.module = module
        self.cluster = cluster
        self.ec2_id = ec2_id

        region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
        if not region:
            module.fail_json(msg=("Region must be specified as a parameter,"
                                  " in EC2_REGION or AWS_REGION environment"
                                  " variables or in boto configuration file"))
        self.ecs = boto3_conn(module, conn_type='client', resource='ecs',
                              region=region, endpoint=ec2_url, **aws_connect_kwargs)

        self.ecs_arn = self._get_ecs_arn()

    def _get_ecs_arn(self):
        try:
            ecs_instances_arns = self.ecs.list_container_instances(cluster=self.cluster)['containerInstanceArns']
            ec2_instances = self.ecs.describe_container_instances(cluster=self.cluster,
                                                                  containerInstances=ecs_instances_arns)['containerInstances']
        except (ClientError, EndpointConnectionError) as e:
            self.module.fail_json(msg="Can't connect to the cluster - %s" % str(e))

        try:
            ecs_arn = next(inst for inst in ec2_instances
                           if inst['ec2InstanceId'] == self.ec2_id)['containerInstanceArn']
        except StopIteration:
            self.module.fail_json(msg="EC2 instance Id not found in ECS cluster - %s" % str(self.cluster))

        return ecs_arn

    def attrs_put(self, attrs):
        """Puts attributes on ECS container instance"""
        try:
            self.ecs.put_attributes(cluster=self.cluster,
                                    attributes=attrs.get_for_ecs_arn(self.ecs_arn))
        except ClientError as e:
            self.module.fail_json(msg=str(e))

    def attrs_delete(self, attrs):
        """Deletes attributes from ECS container instance."""
        try:
            self.ecs.delete_attributes(cluster=self.cluster,
                                       attributes=attrs.get_for_ecs_arn(self.ecs_arn, skip_value=True))
        except ClientError as e:
            self.module.fail_json(msg=str(e))

    def attrs_get_by_name(self, attrs):
        """
        Returns EcsAttributes object containing attributes from ECS container instance with names
        matching to attrs.attributes (EcsAttributes Object).
        """
        attr_objs = [{'targetType': 'container-instance', 'attributeName': attr['name']}
                     for attr in attrs]

        try:
            matched_ecs_targets = [attr_found for attr_obj in attr_objs
                                   for attr_found in self.ecs.list_attributes(cluster=self.cluster, **attr_obj)['attributes']]
        except ClientError as e:
            self.module.fail_json(msg="Can't connect to the cluster - %s" % str(e))

        matched_objs = [target for target in matched_ecs_targets