from typing import Dict, List, Set, Tuple

import unittest
import urllib.parse

from matemat.webserver import RequestArgument, RequestArguments
# noinspection PyProtectedMember
from matemat.webserver.requestargs import _View


class TestRequestArguments(unittest.TestCase):
    """
    Test cases for the RequestArgument class.
    """

    def test_create_default(self):
        """
        Test creation of an empty RequestArgument
        """
        ra = RequestArgument('foo')
        # Name must be set to 1st argument
        self.assertEqual('foo', ra.name)
        # Must be a 0-length array
        self.assertEqual(0, len(ra))
        self.assertFalse(ra.is_scalar)
        self.assertTrue(ra.is_array)
        # Must not be a view
        self.assertFalse(ra.is_view)

    def test_create_str_scalar(self):
        """
        Test creation of a scalar RequestArgument with string value.
        """
        ra = RequestArgument('foo', ('text/plain', 'bar'))
        # Name must be set to 1st argument
        self.assertEqual('foo', ra.name)
        # Must be a scalar, length 1
        self.assertEqual(1, len(ra))
        self.assertTrue(ra.is_scalar)
        self.assertFalse(ra.is_array)
        # Scalar value must be representable both as str and bytes
        self.assertEqual('bar', ra.get_str())
        self.assertEqual(b'bar', ra.get_bytes())
        # Content-Type must be set correctly
        self.assertEqual('text/plain', ra.get_content_type())
        # Using 0 indices must yield the same results
        self.assertEqual('bar', ra.get_str(0))
        self.assertEqual(b'bar', ra.get_bytes(0))
        self.assertEqual('text/plain', ra.get_content_type(0))
        # Using other indices must result in an error
        with self.assertRaises(IndexError):
            ra.get_str(1)
        with self.assertRaises(IndexError):
            ra.get_bytes(1)
        with self.assertRaises(IndexError):
            ra.get_content_type(1)
        # Must not be a view
        self.assertFalse(ra.is_view)

    def test_create_str_scalar_array(self):
        """
        Test creation of a scalar RequestArgument with string value, passing an array instead of a single tuple.
        """
        ra = RequestArgument('foo', [('text/plain', 'bar')])
        # Name must be set to 1st argument
        self.assertEqual('foo', ra.name)
        # Must be a scalar, length 1
        self.assertEqual(1, len(ra))
        self.assertTrue(ra.is_scalar)
        self.assertFalse(ra.is_array)
        # Scalar value must be representable both as str and bytes
        self.assertEqual('bar', ra.get_str())
        self.assertEqual(b'bar', ra.get_bytes())
        # Content-Type must be set correctly
        self.assertEqual('text/plain', ra.get_content_type())
        # Using 0 indices must yield the same results
        self.assertEqual('bar', ra.get_str(0))
        self.assertEqual(b'bar', ra.get_bytes(0))
        self.assertEqual('text/plain', ra.get_content_type(0))
        # Using other indices must result in an error
        with self.assertRaises(IndexError):
            ra.get_str(1)
        with self.assertRaises(IndexError):
            ra.get_bytes(1)
        with self.assertRaises(IndexError):
            ra.get_content_type(1)
        # Must not be a view
        self.assertFalse(ra.is_view)

    def test_create_bytes_scalar(self):
        """
        Test creation of a scalar RequestArgument with bytes value.
        """
        ra = RequestArgument('foo', ('application/octet-stream', b'\x00\x80\xff\xfe'))
        # Name must be set to 1st argument
        self.assertEqual('foo', ra.name)
        # Must be a scalar, length 1
        self.assertEqual(1, len(ra))
        self.assertTrue(ra.is_scalar)
        self.assertFalse(ra.is_array)
        # Conversion to UTF-8 string must fail; bytes representation must work
        with self.assertRaises(UnicodeDecodeError):
            ra.get_str()
        self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes())
        # Content-Type must be set correctly
        self.assertEqual('application/octet-stream', ra.get_content_type())
        # Using 0 indices must yield the same results
        with self.assertRaises(UnicodeDecodeError):
            ra.get_str(0)
        self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(0))
        self.assertEqual('application/octet-stream', ra.get_content_type(0))
        # Using other indices must result in an error
        with self.assertRaises(IndexError):
            ra.get_str(1)
        with self.assertRaises(IndexError):
            ra.get_bytes(1)
        with self.assertRaises(IndexError):
            ra.get_content_type(1)
        # Must not be a view
        self.assertFalse(ra.is_view)

    def test_create_array(self):
        """
        Test creation of an array RequestArgument with mixed str and bytes initial value.
        """
        ra = RequestArgument('foo', [
            ('text/plain', 'bar'),
            ('application/octet-stream', b'\x00\x80\xff\xfe')
        ])
        # Name must be set to 1st argument
        self.assertEqual('foo', ra.name)
        # Must be an array, length 2
        self.assertEqual(2, len(ra))
        self.assertFalse(ra.is_scalar)
        self.assertTrue(ra.is_array)
        # Retrieving values without an index must yield the first element
        self.assertEqual('bar', ra.get_str())
        self.assertEqual(b'bar', ra.get_bytes())
        self.assertEqual('text/plain', ra.get_content_type())
        # The first value must be representable both as str and bytes, and have ctype text/plain
        self.assertEqual('bar', ra.get_str(0))
        self.assertEqual(b'bar', ra.get_bytes(0))
        self.assertEqual('text/plain', ra.get_content_type(0))
        # Conversion of the second value to UTF-8 string must fail; bytes representation must work
        with self.assertRaises(UnicodeDecodeError):
            ra.get_str(1)
        self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(1))
        # The second value's ctype must be correct
        self.assertEqual('application/octet-stream', ra.get_content_type(1))
        # Must not be a view
        self.assertFalse(ra.is_view)

    def test_append_empty_str(self):
        """
        Test appending a str value to an empty RequestArgument.
        """
        # Initialize the empty RequestArgument
        ra = RequestArgument('foo')
        self.assertEqual(0, len(ra))
        self.assertFalse(ra.is_scalar)

        # Append a string value
        ra.append('text/plain', 'bar')
        # New length must be 1, empty array must be converted to scalar
        self.assertEqual(1, len(ra))
        self.assertTrue(ra.is_scalar)
        # Retrieval of the new value must work both in str and bytes representation
        self.assertEqual('bar', ra.get_str())
        self.assertEqual(b'bar', ra.get_bytes())
        # Content type of the new value must be correct
        self.assertEqual('text/plain', ra.get_content_type())
        # Must not be a view
        self.assertFalse(ra.is_view)

    def test_append_empty_bytes(self):
        """
        Test appending a bytes value to an empty RequestArgument.
        """
        # Initialize the empty RequestArgument
        ra = RequestArgument('foo')
        self.assertEqual(0, len(ra))
        self.assertFalse(ra.is_scalar)

        # Append a bytes value
        ra.append('application/octet-stream', b'\x00\x80\xff\xfe')
        # New length must be 1, empty array must be converted to scalar
        self.assertEqual(1, len(ra))
        self.assertTrue(ra.is_scalar)
        # Conversion of the new value to UTF-8 string must fail; bytes representation must work
        with self.assertRaises(UnicodeDecodeError):
            ra.get_str()
        self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes())
        # Content type of the new value must be correct
        self.assertEqual('application/octet-stream', ra.get_content_type())
        # Must not be a view
        self.assertFalse(ra.is_view)

    def test_append_multiple(self):
        """
        Test appending multiple values to an empty RequestArgument.
        """
        # Initialize the empty RequestArgument
        ra = RequestArgument('foo')
        self.assertEqual(0, len(ra))
        self.assertFalse(ra.is_scalar)

        # Append a first value
        ra.append('text/plain', 'bar')
        # New length must be 1, empty array must be converted to scalar
        self.assertEqual(1, len(ra))
        self.assertTrue(ra.is_scalar)
        self.assertEqual(b'bar', ra.get_bytes())

        # Append a second value
        ra.append('application/octet-stream', b'\x00\x80\xff\xfe')
        # New length must be 2, scalar must be converted to array
        self.assertEqual(2, len(ra))
        self.assertFalse(ra.is_scalar)
        self.assertEqual(b'bar', ra.get_bytes(0))
        self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(1))

        # Append a third value
        ra.append('text/plain', 'Hello, World!')
        # New length must be 3, array must remain array
        self.assertEqual(3, len(ra))
        self.assertFalse(ra.is_scalar)
        self.assertEqual(b'bar', ra.get_bytes(0))
        self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(1))
        self.assertEqual(b'Hello, World!', ra.get_bytes(2))

    def test_clear_empty(self):
        """
        Test clearing an empty RequestArgument.
        """
        # Initialize the empty RequestArgument
        ra = RequestArgument('foo')
        self.assertEqual(0, len(ra))
        self.assertFalse(ra.is_scalar)
        ra.clear()
        # Clearing an empty RequestArgument shouldn't have any effect
        self.assertEqual('foo', ra.name)
        self.assertEqual(0, len(ra))
        self.assertFalse(ra.is_scalar)

    def test_clear_scalar(self):
        """
        Test clearing a scalar RequestArgument.
        """
        # Initialize the scalar RequestArgument
        ra = RequestArgument('foo', ('text/plain', 'bar'))
        self.assertEqual(1, len(ra))
        self.assertTrue(ra.is_scalar)
        ra.clear()
        # Clearing a scalar RequestArgument should reduce its size to 0
        self.assertEqual('foo', ra.name)
        self.assertEqual(0, len(ra))
        self.assertFalse(ra.is_scalar)
        with self.assertRaises(IndexError):
            ra.get_str()

    def test_clear_array(self):
        """
        Test clearing an array RequestArgument.
        """
        # Initialize the array RequestArgument
        ra = RequestArgument('foo', [
            ('text/plain', 'bar'),
            ('application/octet-stream', b'\x00\x80\xff\xfe'),
            ('text/plain', 'baz'),
        ])
        self.assertEqual(3, len(ra))
        self.assertFalse(ra.is_scalar)
        ra.clear()
        # Clearing an array RequestArgument should reduce its size to 0
        self.assertEqual('foo', ra.name)
        self.assertEqual(0, len(ra))
        self.assertFalse(ra.is_scalar)
        with self.assertRaises(IndexError):
            ra.get_str()

    def test_iterate_empty(self):
        """
        Test iterating an empty RequestArgument.
        """
        ra = RequestArgument('foo')
        self.assertEqual(0, len(ra))
        # No value must be yielded from iterating an empty instance
        for _ in ra:
            self.fail()

    def test_iterate_scalar(self):
        """
        Test iterating a scalar RequestArgument.
        """
        ra = RequestArgument('foo', ('text/plain', 'bar'))
        self.assertTrue(ra.is_scalar)
        # Counter for the number of iterations
        count: int = 0
        for it in ra:
            # Make sure the yielded value is a scalar view and has the same name as the original instance
            self.assertIsInstance(it, _View)
            self.assertTrue(it.is_view)
            self.assertEqual('foo', it.name)
            self.assertTrue(it.is_scalar)
            count += 1
        # Only one value must be yielded from iterating a scalar instance
        self.assertEqual(1, count)

    def test_iterate_array(self):
        """
        Test iterating an array RequestArgument.
        """
        ra = RequestArgument('foo', [('text/plain', 'bar'), ('abc', b'def'), ('xyz', '1337')])
        self.assertFalse(ra.is_scalar)
        # Container to put the iterated ctypes into
        items: List[str] = list()
        for it in ra:
            # Make sure the yielded values are scalar views and have the same name as the original instance
            self.assertIsInstance(it, _View)
            self.assertTrue(it.is_view)
            self.assertTrue(it.is_scalar)
            # Collect the value's ctype
            items.append(it.get_content_type())
        # Compare collected ctypes with expected result
        self.assertEqual(['text/plain', 'abc', 'xyz'], items)

    def test_slice(self):
        """
        Test slicing an array RequestArgument.
        """
        ra = RequestArgument('foo', [('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h'), ('i', 'j'), ('k', 'l')])
        # Create the sliced view
        sliced = ra[1:4:2]
        # Make sure the sliced value is a view
        self.assertIsInstance(sliced, _View)
        self.assertTrue(sliced.is_view)
        # Make sure the slice has the same name
        self.assertEqual('foo', sliced.name)
        # Make sure the slice has the expected shape (array of the 2nd and 4th scalar in the original)
        self.assertTrue(sliced.is_array)
        self.assertEqual(2, len(sliced))
        self.assertEqual('d', sliced.get_str(0))
        self.assertEqual('h', sliced.get_str(1))

    def test_iterate_sliced(self):
        """
        Test iterating a sliced array RequestArgument.
        """
        ra = RequestArgument('foo', [('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h'), ('i', 'j'), ('k', 'l')])
        # Container to put the iterated ctypes into
        items: List[str] = list()
        # Iterate the sliced view
        for it in ra[1:4:2]:
            # Make sure the yielded values are scalar views and have the same name as the original instance
            self.assertIsInstance(it, _View)
            self.assertTrue(it.is_view)
            self.assertEqual('foo', it.name)
            self.assertTrue(it.is_scalar)
            items.append(it.get_content_type())
        # Make sure the expected values are collected (array of the 2nd and 4th scalar in the original)
        self.assertEqual(['c', 'g'], items)

    def test_index_scalar(self):
        """
        Test indexing of a scalar RequestArgument.
        """
        ra = RequestArgument('foo', ('bar', 'baz'))
        # Index the scalar RequestArgument instance, obtaining an immutable view
        it = ra[0]
        # Make sure the value is a scalar view with the same properties as the original instance
        self.assertIsInstance(it, _View)
        self.assertTrue(it.is_scalar)
        self.assertEqual('foo', it.name)
        self.assertEqual('bar', it.get_content_type())
        self.assertEqual('baz', it.get_str())
        # Make sure other indices don't work
        with self.assertRaises(IndexError):
            _ = ra[1]

    def test_index_array(self):
        """
        Test indexing of an array RequestArgument.
        """
        ra = RequestArgument('foo', [('a', 'b'), ('c', 'd')])
        # Index the array RequestArgument instance, obtaining an immutable view
        it = ra[1]
        # Make sure the value is a scalar view with the same properties as the value in the original instance
        self.assertIsInstance(it, _View)
        self.assertEqual('foo', it.name)
        self.assertEqual('c', it.get_content_type())
        self.assertEqual('d', it.get_str())

    def test_view_immutable(self):
        """
        Test immutability of views.
        """
        ra = RequestArgument('foo', ('bar', 'baz'))
        # Index the scalar RequestArgument instance, obtaining an immutable view
        it = ra[0]
        # Make sure the returned value is a view
        self.assertIsInstance(it, _View)
        # Make sure the returned value is immutable
        with self.assertRaises(TypeError):
            it.append('foo', 'bar')
        with self.assertRaises(TypeError):
            it.clear()

    def test_str_shorthand(self):
        """
        Test the shorthand for get_str(0).
        """
        ra = RequestArgument('foo', ('bar', 'baz'))
        self.assertEqual('baz', str(ra))

    def test_bytes_shorthand(self):
        """
        Test the shorthand for get_bytes(0).
        """
        ra = RequestArgument('foo', ('bar', b'\x00\x80\xff\xfe'))
        self.assertEqual(b'\x00\x80\xff\xfe', bytes(ra))

    # noinspection PyTypeChecker
    def test_insert_garbage(self):
        """
        Test proper handling with non-int indices and non-str/non-bytes data
        :return:
        """
        ra = RequestArgument('foo', 42)
        with self.assertRaises(TypeError):
            str(ra)
        ra = RequestArgument('foo', (None, 42))
        with self.assertRaises(TypeError):
            str(ra)
        with self.assertRaises(TypeError):
            bytes(ra)
        with self.assertRaises(TypeError):
            ra.get_content_type()
        with self.assertRaises(TypeError):
            ra.get_str('foo')
        with self.assertRaises(TypeError):
            ra.get_bytes('foo')
        with self.assertRaises(TypeError):
            ra.get_content_type('foo')

    def test_requestarguments_index(self):
        """
        Make sure indexing a RequestArguments instance creates a new entry on the fly.
        """
        ra = RequestArguments()
        self.assertEqual(0, len(ra))
        self.assertFalse('foo' in ra)
        # Create new entry
        _ = ra['foo']
        self.assertEqual(1, len(ra))
        self.assertTrue('foo' in ra)
        # Already exists, no new entry created
        _ = ra['foo']
        self.assertEqual(1, len(ra))
        # Entry must be empty and mutable, and have the correct name
        self.assertFalse(ra['foo'].is_view)
        self.assertEqual(0, len(ra['foo']))
        self.assertEqual('foo', ra['foo'].name)
        # Key must be a string
        with self.assertRaises(TypeError):
            # noinspection PyTypeChecker
            _ = ra[42]

    def test_requestarguments_attr(self):
        """
        Test attribute access syntactic sugar.
        """
        ra = RequestArguments()
        # Attribute should not exist yet
        with self.assertRaises(KeyError):
            _ = ra.foo
        # Create entry
        _ = ra['foo']
        # Creating entry should have created the attribute
        self.assertEqual('foo', ra.foo.name)
        # Attribute access should yield an immutable view
        self.assertTrue(ra.foo.is_view)

    def test_requestarguments_iterate(self):
        """
        Test iterating a RequestArguments instance.
        """
        # Create an instance with some values
        ra = RequestArguments()
        ra['foo'].append('a', 'b')
        ra['bar'].append('c', 'd')
        ra['foo'].append('e', 'f')
        # Container for test values (name, value)
        items: Set[Tuple[str, str]] = set()
        # Iterate RequestArguments instance, adding the name and value of each to the set
        for a in ra:
            items.add((a.name, str(a)))
        # Compare result with expected value
        self.assertEqual(2, len(items))
        self.assertIn(('foo', 'b'), items)
        self.assertIn(('bar', 'd'), items)

    def test_requestarguments_full_use_case(self):
        """
        Simulate a minimal RequestArguments use case.
        """
        # Create empty RequestArguments instance
        ra = RequestArguments()
        # Parse GET request
        getargs: Dict[str, List[str]] = urllib.parse.parse_qs('foo=42&bar=1337&foo=43&baz=Hello,%20World!')
        # Insert GET arguments into RequestArguments
        for k, vs in getargs.items():
            for v in vs:
                ra[k].append('text/plain', v)
        # Parse POST request
        postargs: Dict[str, List[str]] = urllib.parse.parse_qs('foo=postfoo&postbar=42&foo=postfoo')
        # Insert POST arguments into RequestArguments
        for k, vs in postargs.items():
            # In this implementation, POST args replace GET args
            ra[k].clear()
            for v in vs:
                ra[k].append('text/plain', v)

        # Someplace else: Use the RequestArguments instance.
        self.assertEqual('1337', ra.bar.get_str())
        self.assertEqual('Hello, World!', ra.baz.get_str())
        self.assertEqual('42', ra.postbar.get_str())
        for a in ra.foo:
            self.assertEqual('postfoo', a.get_str())