"""
This module is intended for tests related to unquoting/unescaping in various
formats that are related to turtle, such as ntriples, nquads, trig and n3.
"""

from __future__ import annotations

import itertools
import logging
import re
from typing import Callable, Dict, Iterable, List, Tuple

import pytest

from rdflib.graph import Dataset, Graph
from rdflib.plugins.parsers import ntriples
from rdflib.term import Literal, URIRef
from test.utils.namespace import EGDC

from .utils import GraphHelper

# https://www.w3.org/TR/turtle/#string
string_escape_map = {
    "t": "\t",
    "b": "\b",
    "n": "\n",
    "r": "\r",
    "f": "\f",
    '"': '"',
    "'": "'",
    "\\": "\\",
}


def make_unquote_correctness_pairs() -> List[Tuple[str, str]]:
    """
    Creates pairs of quoted and unquoted strings.
    """
    result = []

    def add_pair(escape: str, unescaped: str) -> None:
        result.append((f"\\{escape}", unescaped))
        result.append((f"\\\\{escape}", f"\\{escape}"))
        result.append((f"\\\\\\{escape}", f"\\{unescaped}"))

    chars = "A1a\\\nøæå"
    for char in chars:
        code_point = ord(char)
        add_pair(f"u{code_point:04x}", char)
        add_pair(f"u{code_point:04X}", char)
        add_pair(f"U{code_point:08x}", char)
        add_pair(f"U{code_point:08X}", char)

    string_escapes = "tbnrf'"
    for char in string_escapes:
        add_pair(f"{char}", string_escape_map[char])

    # special handling because «"» should not appear in string, and add_pair
    # will add it.
    result.append(('\\"', '"'))
    result.append(('\\\\\\"', '\\"'))

    # special handling because «\» should not appear in string, and add_pair
    # will add it.
    result.append(("\\\\", "\\"))
    result.append(("\\\\\\\\", "\\\\"))

    return result


UNQUOTE_CORRECTNESS_PAIRS = make_unquote_correctness_pairs()


def ntriples_unquote_validate(input: str) -> str:
    """
    This function wraps `ntriples.unquote` in a way that ensures that `ntriples.validate` is always `True` when it runs.
    """
    old_validate = ntriples.validate
    try:
        ntriples.validate = True
        return ntriples.unquote(input)
    finally:
        ntriples.validate = old_validate


def ntriples_unquote(input: str) -> str:
    """
    This function wraps `ntriples.unquote` in a way that ensures that `ntriples.validate` is always `False` when it runs.
    """
    old_validate = ntriples.validate
    try:
        ntriples.validate = False
        return ntriples.unquote(input)
    finally:
        ntriples.validate = old_validate


unquoters: Dict[str, Callable[[str], str]] = {
    "ntriples_unquote": ntriples_unquote,
    "ntriples_unquote_validate": ntriples_unquote_validate,
}


def make_unquote_correctness_tests(
    selectors: Iterable[str],
) -> Iterable[Tuple[str, str, str]]:
    """
    This function creates a cartesian product of the selectors and
    `CORRECTNESS_PAIRS` that is suitable for use as pytest parameters.
    """
    for selector in selectors:
        for quoted, unquoted in UNQUOTE_CORRECTNESS_PAIRS:
            yield selector, quoted, unquoted


@pytest.mark.parametrize(
    "unquoter_key, quoted, unquoted", make_unquote_correctness_tests(unquoters.keys())
)
def test_unquote_correctness(
    unquoter_key: str,
    quoted: str,
    unquoted: str,
) -> None:
    """
    Various unquote functions work correctly.
    """
    unquoter = unquoters[unquoter_key]
    assert unquoted == unquoter(quoted)


QUAD_FORMATS = {"nquads"}


@pytest.mark.parametrize(
    "format, quoted, unquoted",
    make_unquote_correctness_tests(["turtle", "ntriples", "nquads"]),
)
def test_parse_correctness(
    format: str,
    quoted: str,
    unquoted: str,
) -> None:
    """
    Quoted strings parse correctly
    """
    if format in QUAD_FORMATS:
        data = f'<example:Subject> <example:Predicate> "{quoted}" <example:Graph>.'
    else:
        data = f'<example:Subject> <example:Predicate> "{quoted}".'
    graph = Dataset(default_union=True)
    graph.parse(data=data, format=format)
    objs = list(graph.objects())
    assert len(objs) == 1
    obj = objs[0]
    assert isinstance(obj, Literal)
    assert isinstance(obj.value, str)
    assert obj.value == unquoted


@pytest.mark.parametrize(
    "format, char, escaped",
    [
        (format, char, escaped)
        for format, (char, escaped) in itertools.product(
            ["turtle"],
            [
                (r"x", r"x"),
                (r"(", r"\("),
                (r")", r"\)"),
            ],
        )
    ],
)
def test_pname_escaping(format: str, char: str, escaped: str) -> None:
    graph = Graph()
    triple = (
        URIRef(EGDC["prefix/John_Doe"]),
        URIRef(EGDC[f"prefix/prop{char}"]),
        Literal("foo", lang="en"),
    )
    graph.bind("egns", EGDC["prefix/"])
    graph.add(triple)
    data = graph.serialize(format=format)
    pattern = re.compile(f"\\segns:prop{re.escape(escaped)}\\s")
    logging.debug(
        "format = %s, char = %r, escaped = %r, pattern = %r, data = %s",
        format,
        char,
        escaped,
        pattern,
        data,
    )
    assert re.search(pattern, data) is not None


# https://www.w3.org/TR/turtle/#grammar-production-PN_LOCAL_ESC
# Not including %, as % should be used for percent encoding.
PN_LOCAL_ESC_CHARS = r"_~.-!$&'()*+,;=/?#@"


@pytest.mark.parametrize(
    "format, char",
    itertools.product(["turtle", "ntriples"], "A2c" + PN_LOCAL_ESC_CHARS),
)
def test_serialize_roundtrip(format: str, char: str) -> None:
    graph = Graph()
    triple = (
        URIRef(EGDC["prefix/John_Doe"]),
        URIRef(EGDC[f"prefix/prop{char}"]),
        Literal("foo", lang="en"),
    )
    graph.add(triple)
    graph.bind("egns", EGDC["prefix/"])
    data = graph.serialize(format=format)
    logging.debug("format = %s, char = %s, data = %s", format, char, data)
    parsed_graph = Graph()
    parsed_graph.parse(data=data, format=format)
    GraphHelper.assert_sets_equals(graph, parsed_graph)
