# Copyright (c) 2020, Manfred Moitzi
# License: MIT License
import pytest
from io import BytesIO
from ezdxf.recover import (
    bytes_loader,
    detect_encoding,
    synced_bytes_loader,
    _detect_dxf_version,
    _search_int,
    _search_float,
    byte_tag_compiler,
)
from ezdxf.lldxf import const
from ezdxf.audit import AuditError

HEADER = """  0
SECTION
  2
HEADER
  9
$ACADVER
  1
AC1027
  9
$ACADMAINTVER
 70
105
  9
$DWGCODEPAGE
  3
ANSI_1252
  9
TEST
  1
ÄÖÜ
"""

MALFORMED_GROUP_CODES = b"""  0x
SECTION
  +2x
HEADER
  \t9\t
$ACADVER
  1.0
AC1027
"""


class TestBytesLoader:
    @pytest.fixture(params=[bytes_loader, synced_bytes_loader])
    def loader(self, request):
        return request.param

    @pytest.fixture
    def data(self):
        return BytesIO(HEADER.encode("latin1"))

    def test_bytes_loader(self, data, loader):
        result = list(loader(data))
        assert len(result) == 10
        assert result[-1] == (1, b"\xc4\xd6\xdc")

    def test_encoding_detector(self, data, loader):
        assert detect_encoding(loader(data)) == "utf8"

    def test_windows_line_endings_CR_LF(self, loader):
        tags = list(loader(BytesIO(b"0\r\n1\r\n0\r\n2\r\n")))
        assert tags == [
            (0, b"1"),
            (0, b"2"),
        ], "Windows line endings CR/LF must be supported."

    def test_linux_and_macos_x_line_endings_LF(self, loader):
        tags = list(bytes_loader(BytesIO(b"0\n1\n0\n2\n")))
        assert tags == [
            (0, b"1"),
            (0, b"2"),
        ], "Linux and MacOS X line endings LF must be supported."

    def test_line_endings_only_CR(self):
        assert (
            list(synced_bytes_loader(BytesIO(b"0\r1\r0\r2\r"))) == []
        ), "MacOS prior to MacOS X line endings CR are not supported."

    def test_malformed_group_codes(self, loader):
        data = BytesIO(MALFORMED_GROUP_CODES)
        tags = list(loader(data))
        assert tags[0] == (0, b"SECTION")
        assert tags[1] == (2, b"HEADER")
        assert tags[2] == (9, b"$ACADVER")
        assert tags[3] == (1, b"AC1027")


MALFORMED_VALUE_TAGS = b"""  70 # int value
  42xyz
40 # float value
  47.11xyz
10 # 3D vertex
 1.0x
20
 1.0y
30
 1.0z
11 # 2D vertex
 2.0x
21
 2.0y
 0
DUMMY
"""


class TestByteTagCompiler:
    def test_malformed_value_tags(self):
        loader = bytes_loader(BytesIO(MALFORMED_VALUE_TAGS))
        msg = []
        tags = list(byte_tag_compiler(loader, messages=msg))
        assert tags[0] == (70, 42)
        assert tags[1] == (40, 47.11)
        assert tags[2] == (10, (1, 1, 1))
        assert tags[3] == (11, (2, 2))
        assert len(msg) == 7

    def test_malformed_integer_tags(self):
        loader = bytes_loader(BytesIO(b"70\n123x\n"))
        msg = []
        tags = list(byte_tag_compiler(loader, messages=msg))
        assert tags[0] == (70, 123)
        assert msg[0][0] == AuditError.INVALID_INTEGER_VALUE

    def test_underlines_in_numbers_are_handled_by_python(self):
        loader = bytes_loader(BytesIO(b"50\n123_000.9\n70\n456_000\n"))
        msg = []
        tags = list(byte_tag_compiler(loader, messages=msg))
        assert tags[0] == (50, 123000.9)
        assert tags[1] == (70, 456000)
        assert len(msg) == 0

    @pytest.mark.parametrize("b", [
        b"50\n1.000 e20\n",
        b"50\n1.000 E20\n",
        b"50\n1.000\te20\n",
        b"50\n1.000\tE20\n",
    ])
    def test_ignore_whitespace_in_floats(self, b):
        loader = bytes_loader(BytesIO(b))
        msg = []
        tags = list(byte_tag_compiler(loader, messages=msg))
        assert tags[0] == (50, 1e20)
        assert msg[0][0] == AuditError.INVALID_FLOATING_POINT_VALUE


OUT_OF_SYNC_TAGS = """
  0
SECTION
  2
HEADER

  9
$ACADVER
  1
AC1027
  9
$ACADMAINTVER
 70
105
  9
$DWGCODEPAGE

  3
ANSI_1252
  9
TEST
  1
test
"""


def test_out_of_sync_tags():
    result = list(synced_bytes_loader(BytesIO(OUT_OF_SYNC_TAGS.encode())))
    assert len(result) == 10


class TestDetectDXFVersion:
    def test_missing_dxf_version_is_r12(self):
        assert _detect_dxf_version([]) == "AC1009"

    def test_detect_r12(self):
        assert _detect_dxf_version([(9, "$ACADVER"), (3, "AC1009")]) == "AC1009"

    @pytest.mark.parametrize("ver", list(const.acad_release.keys()))
    def test_detect_all_supported_versions(self, ver):
        assert _detect_dxf_version([(9, "$ACADVER"), (3, ver)]) == ver

    def test_detect_any_well_formed_version(self):
        assert _detect_dxf_version([(9, "$ACADVER"), (3, "AC9999")]) == "AC9999"

    @pytest.mark.parametrize(
        "ver", ["AC10x9", "XC1009", "0000", "AC10240", 0, None]
    )
    def test_malformed_version_is_r12(self, ver):
        assert _detect_dxf_version([(9, "$ACADVER"), (3, ver)]) == "AC1009"

    @pytest.mark.parametrize(
        "ver",
        [
            " AC1015",
            "AC1015 ",
            "  AC1015  ",
        ],
    )
    def test_strip_whitespace_from_version_string(self, ver):
        assert _detect_dxf_version([(9, "$ACADVER"), (3, ver)]) == "AC1015"


class TestSearchIntConverter:
    @pytest.mark.parametrize("s", ["0", "00", "+0", "-0"])
    def test_valid_zero_int(self, s):
        assert _search_int(s) == 0

    @pytest.mark.parametrize("s", ["+1", "1", "01"])
    def test_valid_int(self, s):
        assert _search_int(s) == 1

    @pytest.mark.parametrize("s", [" 1", "\t1", "\r1", " \r1"])
    def test_ignore_whitespace_in_front_of_int(self, s):
        assert _search_int(s) == 1

    @pytest.mark.parametrize("s", ["1 ", "1\t", "1\r", "1 \r"])
    def test_ignore_whitespace_behind_int(self, s):
        assert _search_int(s) == 1

    @pytest.mark.parametrize(
        "s",
        [
            "1.",
            "1,",
            "1a",
            "1-",
            "1+",
            "1$",
            "1 1",
            "+1 x fd fgg",
            "\t\r+1_ ",
        ],
    )
    def test_ignore_invalid_chars_behind_int(self, s):
        assert _search_int(s) == 1

    @pytest.mark.parametrize(
        "b",
        [
            b"1.",
            b"1,",
            b"1a",
            b"1-",
            b"\t\r+1_ ",
        ],
    )
    def test_if_works_with_bytes(self, b):
        assert _search_int(b) == 1


class TestSearchFloatConverter:
    @pytest.mark.parametrize("s", ["0", "0.0", "+0.", "-0."])
    def test_valid_zero(self, s):
        assert _search_float(s) == 0.0

    @pytest.mark.parametrize(
        "s",
        ["1", "+1", "1.", "1.0", "1e0", "1.e0", "1.0e0", "1E0", "1e+0", "1e-0"],
    )
    def test_valid_positive_floats(self, s):
        assert _search_float(s) == 1.0

    @pytest.mark.parametrize(
        "s",
        [
            "-1",
            "-1.",
            "-1.0",
            "-1e0",
            "-1.e0",
            "-1.0e0",
        ],
    )
    def test_valid_negative_floats(self, s):
        assert _search_float(s) == -1.0

    def test_negative_exponent(self):
        assert _search_float("1e-1") == 0.1
        assert _search_float("-1e-1") == -0.1

    @pytest.mark.parametrize("s", [" 1.0", "\t1.0", "\r1.0", " \r1.0"])
    def test_ignore_whitespace_in_front_of_float(self, s):
        assert _search_float(s) == 1.0

    @pytest.mark.parametrize("s", ["1.0 ", "1.\t", "1.\r", "1. \r", "1e0 \t"])
    def test_ignore_whitespace_behind_float(self, s):
        assert _search_float(s) == 1.0

    @pytest.mark.parametrize(
        "s",
        [
            "1,",
            "1a",
            "1-",
            "1+",
            "1$",
            "1 1",
            "+1 x fd fgg",
            "\t\r+1_ ",
            "1.,",
            "1.a",
            "1.-",
            "1.+",
            "1.$",
            "1. 1",
            "+1. x fd fgg",
            "\t\r+1._ ",
            "1e0,",
            "1e0a",
            "1e0-",
            "1e0+",
            "1e0$",
            "1e0 1",
            "+1e0 x fd fgg",
            "\t\r+1e0_ ",
            "  1e+0xx",
            "  1e-0xx",
            "  1E+0xx",
            "  1E-0xx",
        ],
    )
    def test_ignore_invalid_chars_behind_float(self, s):
        assert _search_float(s) == 1.0

    @pytest.mark.parametrize(
        "b",
        [
            b"1,",
            b"1a",
            b"1-",
            b"1+",
            b"1$",
            b"1 1",
            b"+1 x fd fgg",
            b"\t\r+1_ ",
        ],
    )
    def test_if_works_bytes(self, b):
        assert _search_float(b) == 1.0
