# KEKS -- Python KEKS implementation
# Copyright (C) 2024-2025 Anton Rudenko <rudenko.ad@phystech.edu>
#               2024-2025 Sergey Matveev <stargrave@stargrave.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program.  If not, see <http://www.gnu.org/licenses/>.

from unittest import TestCase

from hypothesis import given
from hypothesis.strategies import binary
from hypothesis.strategies import integers

from keks import DecodeError
from keks import dumps
from keks import loads
from keks import NotEnoughData
from tests.strategies import junk_st
from tests.strategies import unicode_allowed


TagStr: int = 0x80
TagUTF8: int = 0x40

# Here go invalid UTF-8 byte(s) strategies. We can fail in following
# ways during decoding:
# * invalid start byte -- hypothesis generates integers that will
#   satisfy that fail
# * invalid continuation byte -- we strip last byte of multibyte
#   sequences
# * unexpected end of data -- we strip the last byte of our data

# 110xxxxx 10xxxxxx(STRIPPED)
invalid_utf8_2byte_s = integers(min_value=0, max_value=(1 << 5) - 1).map(
    lambda v: bytes(bytearray([1 << 7 | 1 << 6 | v]))
)

# 1110xxxx 10xxxxxx 10xxxxxx(STRIPPED)
invalid_utf8_3byte_s = integers(min_value=0, max_value=(1 << 10) - 1).map(
    lambda v: bytes(bytearray([
        (1 << 7 | 1 << 6 | 1 << 5 | (v >> 6)),
        (1 << 7 | ((1 << 6) - 1) & v),
    ]))
)


class TestStr(TestCase):
    @given(junk_st)
    def test_empty(self, junk: bytes) -> None:
        encoded: bytes = dumps("")
        self.assertSequenceEqual(encoded, b"\xc0")
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, "")
        self.assertSequenceEqual(tail, junk)

    @given(junk_st)
    def test_hello_world(self, junk: bytes) -> None:
        s: str = "hello world"
        encoded: bytes = dumps(s)
        self.assertSequenceEqual(encoded, bytes.fromhex("CB") + b"hello world")
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, s)
        self.assertSequenceEqual(tail, junk)

    @given(junk_st)
    def test_привет_мир(self, junk: bytes) -> None:
        s: str = "привет мир"
        encoded: bytes = dumps(s)
        self.assertSequenceEqual(
            encoded,
            bytes.fromhex("D3") + "привет мир".encode("utf-8"),
        )
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, s)
        self.assertSequenceEqual(tail, junk)

    @given(junk_st)
    def test_len62(self, junk: bytes) -> None:
        s: str = "a" * 62
        encoded = dumps(s)
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, s)
        self.assertSequenceEqual(tail, junk)

    @given(junk_st)
    def test_len318(self, junk: bytes) -> None:
        assert 318 == 62 + 255 + 1
        s: str = "a" * 318
        encoded = dumps(s)
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, s)
        self.assertSequenceEqual(tail, junk)

    @given(junk_st)
    def test_len65853(self, junk: bytes) -> None:
        assert 65853 == 62 + 255 + 65535 + 1
        s: str = "a" * 65853
        encoded = dumps(s)
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, s)
        self.assertSequenceEqual(tail, junk)

    def test_throws_when_null_byte_in_utf8(self) -> None:
        with self.assertRaises(DecodeError) as err:
            result, tail = loads(b"\xc5he\x00\x01\x02")
        self.assertEqual(str(err.exception), "null byte in UTF-8")

    @given(invalid_utf8_2byte_s)
    def test_throws_when_invalid_utf_2bytes(self, invalid_utf: bytes) -> None:
        with self.assertRaises(DecodeError) as err:
            encoded = dumps("hello")[:-1] + invalid_utf
            result, tail = loads(encoded)
        self.assertEqual(str(err.exception), "invalid UTF-8")

    @given(invalid_utf8_3byte_s)
    def test_throws_when_invalid_utf_3bytes(self, invalid_utf: bytes) -> None:
        with self.assertRaises(DecodeError) as err:
            encoded = dumps("hello")[:-2] + invalid_utf
            result, tail = loads(encoded)
        self.assertEqual(str(err.exception), "invalid UTF-8")

    def test_throws_when_not_enough_data(self) -> None:
        encoded = b"\xc5he"
        with self.assertRaises(NotEnoughData) as err:
            loads(encoded)
        self.assertEqual(err.exception.n, 5-2)

    def test_throws_when_not_enough_data_for_length_8(self) -> None:
        long_string = "a" * 100
        encoded = dumps(long_string)[:1]
        with self.assertRaises(NotEnoughData) as err:
            loads(encoded)
        self.assertEqual(err.exception.n, 1)

    def test_throws_when_not_enough_data_for_length_16(self) -> None:
        long_string = "a" * 318
        encoded = dumps(long_string)[:2]
        with self.assertRaises(NotEnoughData) as err:
            loads(encoded)
        self.assertEqual(err.exception.n, 1)

    def test_throws_when_not_enough_data_for_length_64(self) -> None:
        long_string = "a" * 65853
        encoded = dumps(long_string)[:2]
        with self.assertRaises(NotEnoughData) as err:
            loads(encoded)
        self.assertEqual(err.exception.n, 7)

    @given(unicode_allowed)
    def test_symmetric(self, s: str):
        decoded, _ = loads(dumps(s))
        self.assertEqual(decoded, s)


class TestBin(TestCase):
    @given(junk_st)
    def test_empty(self, junk: bytes) -> None:
        encoded: bytes = dumps(b"")
        self.assertSequenceEqual(encoded, b"\x80")
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, b"")
        self.assertSequenceEqual(tail, junk)

    @given(junk_st)
    def test_1234(self, junk: bytes) -> None:
        s: bytes = b"\x01\x02\x03\x04"
        encoded = dumps(s)
        self.assertSequenceEqual(encoded, b"\x84\x01\x02\x03\x04")
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, s)
        self.assertSequenceEqual(tail, junk)

    @given(junk_st)
    def test_len62(self, junk: bytes) -> None:
        s: bytes = b"a" * 62
        encoded = dumps(s)
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, s)
        self.assertSequenceEqual(tail, junk)

    @given(junk_st)
    def test_len318(self, junk: bytes) -> None:
        assert 318 == 62 + 255 + 1
        s: bytes = b"a" * 318
        encoded = dumps(s)
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, s)
        self.assertSequenceEqual(tail, junk)

    @given(junk_st)
    def test_len65853(self, junk: bytes) -> None:
        assert 65853 == 62 + 255 + 65535 + 1
        s: bytes = b"a" * 65853
        encoded = dumps(s)
        decoded, tail = loads(encoded + junk)
        self.assertSequenceEqual(decoded, s)
        self.assertSequenceEqual(tail, junk)

    def test_throws_when_not_enough_data(self) -> None:
        encoded = b"\x85he"
        with self.assertRaises(NotEnoughData) as err:
            loads(encoded)
        self.assertEqual(err.exception.n, 5-2)

    def test_throws_when_not_enough_data_for_length_8(self) -> None:
        long_string = b"a" * 100
        encoded = dumps(long_string)[:1]
        with self.assertRaises(NotEnoughData) as err:
            loads(encoded)
        self.assertEqual(err.exception.n, 1)

    def test_throws_when_not_enough_data_for_length_16(self) -> None:
        long_string = b"a" * 318
        encoded = dumps(long_string)[:2]
        with self.assertRaises(NotEnoughData) as err:
            loads(encoded)
        self.assertEqual(err.exception.n, 1)

    def test_throws_when_not_enough_data_for_length_64(self) -> None:
        long_string = b"a" * 65853
        encoded = dumps(long_string)[:2]
        with self.assertRaises(NotEnoughData) as err:
            loads(encoded)
        self.assertEqual(err.exception.n, 7)

    @given(binary())
    def test_symmetric(self, s: bytes):
        decoded, _ = loads(dumps(s))
        self.assertEqual(decoded, s)
