import base64
import json
import os

import re
from datetime import datetime, timedelta, timezone

import pytest

from ...constants import *  # NOQA
from ...helpers.argparsing import ArgumentTypeError
from ...helpers.parseformat import (
    bin_to_hex,
    binary_to_json,
    text_to_json,
    Location,
    archivename_validator,
    text_validator,
    format_file_size,
    parse_file_size,
    interval,
    int_or_interval,
    partial_format,
    clean_lines,
    format_line,
    PlaceholderError,
    replace_placeholders,
    swidth_slice,
    eval_escapes,
    ChunkerParams,
)
from ...helpers.time import format_timedelta, parse_timestamp
from ...platformflags import is_win32


def test_bin_to_hex():
    assert bin_to_hex(b"") == ""
    assert bin_to_hex(b"\x00\x01\xff") == "0001ff"


@pytest.mark.parametrize(
    "key,value",
    [("key", b"\x00\x01\x02\x03"), ("key", b"\x00\x01\x02"), ("key", b"\x00\x01"), ("key", b"\x00"), ("key", b"")],
)
def test_binary_to_json(key, value):
    key_b64 = key + "_b64"
    d = binary_to_json(key, value)
    assert key_b64 in d
    assert base64.b64decode(d[key_b64]) == value


@pytest.mark.parametrize(
    "key,value,strict",
    [
        ("key", "abc", True),
        ("key", "äöü", True),
        ("key", "", True),
        ("key", b"\x00\xff".decode("utf-8", errors="surrogateescape"), False),
        ("key", "äöü".encode("latin1").decode("utf-8", errors="surrogateescape"), False),
    ],
)
def test_text_to_json(key, value, strict):
    key_b64 = key + "_b64"
    d = text_to_json(key, value)
    value_b = value.encode("utf-8", errors="surrogateescape")
    if strict:
        # No surrogate escapes; just Unicode text.
        assert key in d
        assert d[key] == value_b.decode("utf-8", errors="strict")
        assert d[key].encode("utf-8", errors="strict") == value_b
        assert key_b64 not in d  # Not needed; pure valid Unicode.
    else:
        # Requires surrogate escapes. The text has replacement characters; Base64 representation is present.
        assert key in d
        assert d[key] == value.encode("utf-8", errors="replace").decode("utf-8", errors="strict")
        assert d[key].encode("utf-8", errors="strict") == value.encode("utf-8", errors="replace")
        assert key_b64 in d
        assert base64.b64decode(d[key_b64]) == value_b


class TestLocationWithoutEnv:
    def test_ssh(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        assert (
            repr(Location("ssh://user@host:1234//absolute/path"))
            == "Location(proto='ssh', user='user', pass=None, host='host', port=1234, path='/absolute/path')"
        )
        assert (
            repr(Location("ssh://user@host:1234/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='host', port=1234, path='relative/path')"
        )
        assert (
            repr(Location("ssh://user@host/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='host', port=None, path='relative/path')"
        )
        assert (
            repr(Location("ssh://user@[::]:1234/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='::', port=1234, path='relative/path')"
        )
        assert (
            repr(Location("ssh://user@[::]/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='::', port=None, path='relative/path')"
        )
        assert (
            repr(Location("ssh://user@[2001:db8::]:1234/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='2001:db8::', port=1234, path='relative/path')"
        )
        assert (
            repr(Location("ssh://user@[2001:db8::]/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='2001:db8::', port=None, path='relative/path')"
        )
        assert (
            repr(Location("ssh://user@[2001:db8::c0:ffee]:1234/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='2001:db8::c0:ffee', port=1234, path='relative/path')"  # noqa: E501
        )
        assert (
            repr(Location("ssh://user@[2001:db8::c0:ffee]/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='2001:db8::c0:ffee', port=None, path='relative/path')"  # noqa: E501
        )
        assert (
            repr(Location("ssh://user@[2001:db8::192.0.2.1]:1234/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='2001:db8::192.0.2.1', port=1234, path='relative/path')"  # noqa: E501
        )
        assert (
            repr(Location("ssh://user@[2001:db8::192.0.2.1]/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='2001:db8::192.0.2.1', port=None, path='relative/path')"  # noqa: E501
        )
        assert (
            repr(Location("ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, "
            "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='relative/path')"
        )
        assert (
            repr(Location("ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]:1234/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, "
            "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='relative/path')"
        )

    def test_rest(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        assert (
            repr(Location("rest://user@host:1234//absolute/path"))
            == "Location(proto='rest', user='user', pass=None, host='host', port=1234, path='/absolute/path')"
        )
        assert (
            repr(Location("rest://user@host:1234/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='host', port=1234, path='relative/path')"
        )
        assert (
            repr(Location("rest://user@host/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='host', port=None, path='relative/path')"
        )
        assert (
            repr(Location("rest://user@[::]:1234/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='::', port=1234, path='relative/path')"
        )
        assert (
            repr(Location("rest://user@[::]/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='::', port=None, path='relative/path')"
        )
        assert (
            repr(Location("rest://user@[2001:db8::]:1234/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='2001:db8::', port=1234, path='relative/path')"
        )
        assert (
            repr(Location("rest://user@[2001:db8::]/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='2001:db8::', port=None, path='relative/path')"
        )
        assert (
            repr(Location("rest://user@[2001:db8::c0:ffee]:1234/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='2001:db8::c0:ffee', port=1234, path='relative/path')"  # noqa: E501
        )
        assert (
            repr(Location("rest://user@[2001:db8::c0:ffee]/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='2001:db8::c0:ffee', port=None, path='relative/path')"  # noqa: E501
        )
        assert (
            repr(Location("rest://user@[2001:db8::192.0.2.1]:1234/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='2001:db8::192.0.2.1', port=1234, path='relative/path')"  # noqa: E501
        )
        assert (
            repr(Location("rest://user@[2001:db8::192.0.2.1]/relative/path"))
            == "Location(proto='rest', user='user', pass=None, host='2001:db8::192.0.2.1', port=None, path='relative/path')"  # noqa: E501
        )
        assert (
            repr(Location("rest://user@[2a02:0001:0002:0003:0004:0005:0006:0007]/relative/path"))
            == "Location(proto='rest', user='user', pass=None, "
            "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='relative/path')"
        )
        assert (
            repr(Location("rest://user@[2a02:0001:0002:0003:0004:0005:0006:0007]:1234/relative/path"))
            == "Location(proto='rest', user='user', pass=None, "
            "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='relative/path')"
        )
        assert (
            repr(Location("rest:///relative/path"))
            == "Location(proto='rest', user=None, pass=None, host=None, port=None, path='relative/path')"
        )
        assert (
            repr(Location("rest:////absolute/path"))
            == "Location(proto='rest', user=None, pass=None, host=None, port=None, path='/absolute/path')"
        )

    # For the protocols handled (parsed + validated) by borgstore itself, borg only detects
    # the scheme and passes the raw URL through; it no longer extracts user/host/port/path.

    def test_s3(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        loc = Location("s3:/test/path")
        assert loc.proto == "s3"
        assert (loc.user, loc.host, loc.port, loc.path) == (None, None, None, None)
        assert loc.processed == "s3:/test/path"
        # credentials in the URL are stripped from canonical_path (security state file / logs)
        assert Location("s3:profile@http://172.28.52.116:9000/test/path").canonical_path() == (
            "s3:http://172.28.52.116:9000/test/path"
        )
        assert Location("s3:user:pass@http://172.28.52.116:9000/test/path").canonical_path() == (
            "s3:http://172.28.52.116:9000/test/path"
        )
        assert Location("b2:user:pass@https://s3.us-east-005.backblazeb2.com/test/path").canonical_path() == (
            "b2:https://s3.us-east-005.backblazeb2.com/test/path"
        )

    def test_rclone(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        loc = Location("rclone:remote:path")
        assert loc.proto == "rclone"
        assert (loc.user, loc.host, loc.port, loc.path) == (None, None, None, None)
        assert loc.processed == "rclone:remote:path"
        assert loc.canonical_path() == "rclone:remote:path"

    def test_sftp(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        loc = Location("sftp://user@host:1234/rel/path")
        assert loc.proto == "sftp"
        assert (loc.user, loc.host, loc.port, loc.path) == (None, None, None, None)
        assert loc.processed == "sftp://user@host:1234/rel/path"
        # credentials stripped from canonical_path
        assert loc.canonical_path() == "sftp://host:1234/rel/path"

    def test_http(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        loc = Location("http://user:pass@host:1234/")
        assert loc.proto == "http"
        assert (loc.user, loc.host, loc.port, loc.path) == (None, None, None, None)
        assert loc.processed == "http://user:pass@host:1234/"
        # credentials stripped from canonical_path
        assert loc.canonical_path() == "http://host:1234/"

    def test_file(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        url = "file:///c:/repo/path" if is_win32 else "file:///repo/path"
        path = "c:/repo/path" if is_win32 else "/repo/path"
        assert (
            repr(Location(url)) == f"Location(proto='file', user=None, pass=None, host=None, port=None, path='{path}')"
        )

    @pytest.mark.skipif(is_win32, reason="still broken")
    def test_smb(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        assert (
            repr(Location("file:////server/share/path"))
            == "Location(proto='file', user=None, pass=None, host=None, port=None, path='//server/share/path')"
        )

    def test_folder(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        rel_path = "path"
        abs_path = os.path.abspath(rel_path)
        assert (
            repr(Location(rel_path))
            == f"Location(proto='file', user=None, pass=None, host=None, port=None, path='{abs_path}')"
        )

    @pytest.mark.skipif(is_win32, reason="Windows has drive letters in abs paths")
    def test_abspath(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        assert (
            repr(Location("/some/absolute/path"))
            == "Location(proto='file', user=None, pass=None, host=None, port=None, path='/some/absolute/path')"
        )
        assert (
            repr(Location("/some/../absolute/path"))
            == "Location(proto='file', user=None, pass=None, host=None, port=None, path='/absolute/path')"
        )

    def test_relpath(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        # For a local path, Borg creates a Location instance with an absolute path.
        rel_path = "relative/path"
        abs_path = os.path.abspath(rel_path)
        assert (
            repr(Location(rel_path))
            == f"Location(proto='file', user=None, pass=None, host=None, port=None, path='{abs_path}')"
        )
        assert (
            repr(Location("ssh://user@host/relative/path"))
            == "Location(proto='ssh', user='user', pass=None, host='host', port=None, path='relative/path')"
        )

    @pytest.mark.skipif(is_win32, reason="Windows does not support colons in paths")
    def test_with_colons(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        assert (
            repr(Location("/abs/path:w:cols"))
            == "Location(proto='file', user=None, pass=None, host=None, port=None, path='/abs/path:w:cols')"
        )
        assert (
            repr(Location("file:///abs/path:w:cols"))
            == "Location(proto='file', user=None, pass=None, host=None, port=None, path='/abs/path:w:cols')"
        )
        assert (
            repr(Location("ssh://user@host/abs/path:w:cols"))
            == "Location(proto='ssh', user='user', pass=None, host='host', port=None, path='abs/path:w:cols')"
        )

    def test_canonical_path(self, monkeypatch):
        monkeypatch.delenv("BORG_REPO", raising=False)
        locations = [
            "relative/path",
            "ssh://host/relative/path",
            "ssh://host//absolute/path",
            "ssh://user@host:1234/relative/path",
            "sftp://host/relative/path",
            "sftp://host//absolute/path",
            "sftp://user@host:1234/relative/path",
            "rclone:remote:path",
        ]
        locations.insert(1, "c:/absolute/path" if is_win32 else "/absolute/path")
        locations.insert(2, "file:///c:/absolute/path" if is_win32 else "file:///absolute/path")
        for location in locations:
            assert (
                Location(location).canonical_path() == Location(Location(location).canonical_path()).canonical_path()
            ), ("failed: %s" % location)

    def test_bad_syntax(self):
        with pytest.raises(ValueError):
            # This is invalid due to the second colon. Correct: 'ssh://user@host/path'.
            Location("ssh://user@host:/path")


@pytest.mark.parametrize(
    "name",
    [
        "foobar",
        # Placeholders
        "foobar-{now}",
    ],
)
def test_archivename_ok(name):
    assert archivename_validator(name) == name


@pytest.mark.parametrize(
    "name",
    [
        "",  # too short
        "x" * 201,  # too long
        # Invalid characters:
        "foo/bar",
        "foo\\bar",
        ">foo",
        "<foo",
        "|foo",
        'foo"bar',
        "foo?",
        "*bar",
        "foo\nbar",
        "foo\0bar",
        # Leading/trailing blanks
        " foo",
        "bar  ",
        # Contains surrogate escapes
        "foo\udc80bar",
        "foo\udcffbar",
    ],
)
def test_archivename_invalid(name):
    with pytest.raises(ArgumentTypeError):
        archivename_validator(name)


@pytest.mark.parametrize("text", ["", "single line", "multi\nline\ncomment"])
def test_text_ok(text):
    assert text_validator(name="text", max_length=100)(text) == text


@pytest.mark.parametrize(
    "text",
    [
        "x" * 101,  # too long
        # Invalid characters:
        "foo\0bar",
        # Contains surrogate escapes
        "foo\udc80bar",
        "foo\udcffbar",
    ],
)
def test_text_invalid(text):
    invalid_ctrl_chars = "".join(chr(i) for i in range(32))
    tv = text_validator(name="text", max_length=100, min_length=1, invalid_ctrl_chars=invalid_ctrl_chars)
    with pytest.raises(ArgumentTypeError):
        tv(text)


def test_format_timedelta():
    t0 = datetime(2001, 1, 1, 10, 20, 3, 0)
    t1 = datetime(2001, 1, 1, 12, 20, 4, 100000)
    assert format_timedelta(t1 - t0) == "2 hours 1.100 seconds"


@pytest.mark.parametrize(
    "timeframe, num_secs",
    [
        ("0S", timedelta(seconds=0)),
        ("5S", timedelta(seconds=5)),
        ("2M", timedelta(minutes=2)),
        ("1H", timedelta(hours=1)),
        ("1d", timedelta(days=1)),
        ("1w", timedelta(days=7)),
        ("1m", timedelta(days=31)),
        ("1y", timedelta(days=365)),
    ],
)
def test_interval(timeframe, num_secs):
    assert interval(timeframe) == num_secs


@pytest.mark.parametrize(
    "invalid_interval, error_tuple",
    [
        ("H", ('Invalid number "": expected nonnegative integer',)),
        ("-1d", ('Invalid number "-1": expected nonnegative integer',)),
        ("food", ('Invalid number "foo": expected nonnegative integer',)),
    ],
)
def test_interval_time_unit(invalid_interval, error_tuple):
    with pytest.raises(ArgumentTypeError) as exc:
        interval(invalid_interval)
    assert exc.value.args == error_tuple


@pytest.mark.parametrize(
    "invalid_input, error_regex",
    [
        ("x", r'^Unexpected time unit "x": choose from'),
        ("-1t", r'^Unexpected time unit "t": choose from'),
        ("fool", r'^Unexpected time unit "l": choose from'),
        ("abc", r'^Unexpected time unit "c": choose from'),
        (" abc ", r'^Unexpected time unit " ": choose from'),
    ],
)
def test_interval_invalid_time_format(invalid_input, error_regex):
    with pytest.raises(ArgumentTypeError) as exc:
        interval(invalid_input)
    assert re.search(error_regex, exc.value.args[0])


@pytest.mark.parametrize(
    "input, result",
    [
        ("0", 0),
        ("5", 5),
        (" 999 ", 999),
        ("-1", -1),
        ("all", -1),
        ("0S", timedelta(seconds=0)),
        ("5S", timedelta(seconds=5)),
        ("1m", timedelta(days=31)),
        # already-converted values (jsonargparse idempotency)
        (0, 0),
        (5, 5),
        (timedelta(seconds=5), timedelta(seconds=5)),
        (timedelta(days=31), timedelta(days=31)),
    ],
)
def test_int_or_interval(input, result):
    assert int_or_interval(input) == result


@pytest.mark.parametrize(
    "invalid_input, error_regex",
    [
        ("H", r"Value is neither an integer nor an interval:"),
        ("-1d", r"Value is neither an integer nor an interval:"),
        ("food", r"Value is neither an integer nor an interval:"),
    ],
)
def test_int_or_interval_time_unit(invalid_input, error_regex):
    with pytest.raises(ArgumentTypeError) as exc:
        int_or_interval(invalid_input)
    assert re.search(error_regex, exc.value.args[0])


def test_parse_timestamp():
    assert parse_timestamp("2015-04-19T20:25:00.226410") == datetime(2015, 4, 19, 20, 25, 0, 226410, timezone.utc)
    assert parse_timestamp("2015-04-19T20:25:00") == datetime(2015, 4, 19, 20, 25, 0, 0, timezone.utc)


@pytest.mark.parametrize(
    "size, fmt",
    [
        (0, "0 B"),  # No rounding necessary for these.
        (1, "1 B"),
        (142, "142 B"),
        (999, "999 B"),
        (1000, "1.00 kB"),  # rounding starts here
        (1001, "1.00 kB"),  # should be rounded away
        (1234, "1.23 kB"),  # should be rounded down
        (1235, "1.24 kB"),  # should be rounded up
        (1010, "1.01 kB"),  # rounded down as well
        (999990000, "999.99 MB"),  # rounded down
        (999990001, "999.99 MB"),  # rounded down
        (999995000, "1.00 GB"),  # Rounded up to the next unit.
        (10**6, "1.00 MB"),  # and all the remaining units, megabytes
        (10**9, "1.00 GB"),  # gigabytes
        (10**12, "1.00 TB"),  # terabytes
        (10**15, "1.00 PB"),  # petabytes
        (10**18, "1.00 EB"),  # exabytes
        (10**21, "1.00 ZB"),  # zettabytes
        (10**24, "1.00 YB"),  # yottabytes
        (-1, "-1 B"),  # negative value
        (-1010, "-1.01 kB"),  # negative value with rounding
    ],
)
def test_file_size(size, fmt):
    """test the size formatting routines"""
    assert format_file_size(size) == fmt


@pytest.mark.parametrize(
    "size, fmt",
    [
        (0, "0 B"),
        (2**0, "1 B"),
        (2**10, "1.00 KiB"),
        (2**20, "1.00 MiB"),
        (2**30, "1.00 GiB"),
        (2**40, "1.00 TiB"),
        (2**50, "1.00 PiB"),
        (2**60, "1.00 EiB"),
        (2**70, "1.00 ZiB"),
        (2**80, "1.00 YiB"),
        (-(2**0), "-1 B"),
        (-(2**10), "-1.00 KiB"),
        (-(2**20), "-1.00 MiB"),
    ],
)
def test_file_size_iec(size, fmt):
    """test the size formatting routines"""
    assert format_file_size(size, iec=True) == fmt


@pytest.mark.parametrize(
    "original_size, formatted_size",
    [
        (1234, "1.2 kB"),  # rounded down
        (1254, "1.3 kB"),  # rounded up
        (999990000, "1.0 GB"),  # and not 999.9 MB or 1000.0 MB
    ],
)
def test_file_size_precision(original_size, formatted_size):
    assert format_file_size(original_size, precision=1) == formatted_size


@pytest.mark.parametrize("size, fmt", [(0, "0 B"), (1, "+1 B"), (1234, "+1.23 kB"), (-1, "-1 B"), (-1234, "-1.23 kB")])
def test_file_size_sign(size, fmt):
    assert format_file_size(size, sign=True) == fmt


@pytest.mark.parametrize(
    "string, value", [("1", 1), ("20", 20), ("5K", 5000), ("1.75M", 1750000), ("1e+9", 1e9), ("-1T", -1e12)]
)
def test_parse_file_size(string, value):
    assert parse_file_size(string) == int(value)


@pytest.mark.parametrize("string", ("", "5 Äpfel", "4E", "2229 bit", "1B"))
def test_parse_file_size_invalid(string):
    with pytest.raises(ValueError):
        parse_file_size(string)


@pytest.mark.parametrize(
    "fmt, items_map, expected_result",
    [
        ("{space:10}", {"space": " "}, " " * 10),
        ("{foobar}", {"bar": "wrong", "foobar": "correct"}, "correct"),
        ("{unknown_key}", {}, "{unknown_key}"),
        ("{key}{{escaped_key}}", {}, "{key}{{escaped_key}}"),
        ("{{escaped_key}}", {"escaped_key": 1234}, "{{escaped_key}}"),
    ],
)
def test_partial_format(fmt, items_map, expected_result):
    assert partial_format(fmt, items_map) == expected_result


def test_clean_lines():
    conf = """\
#comment
data1 #data1
data2

 data3
""".splitlines(
        keepends=True
    )
    assert list(clean_lines(conf)) == ["data1 #data1", "data2", "data3"]
    assert list(clean_lines(conf, lstrip=False)) == ["data1 #data1", "data2", " data3"]
    assert list(clean_lines(conf, rstrip=False)) == ["data1 #data1\n", "data2\n", "data3\n"]
    assert list(clean_lines(conf, remove_empty=False)) == ["data1 #data1", "data2", "", "data3"]
    assert list(clean_lines(conf, remove_comments=False)) == ["#comment", "data1 #data1", "data2", "data3"]


def test_format_line():
    data = dict(foo="bar baz")
    assert format_line("", data) == ""
    assert format_line("{foo}", data) == "bar baz"
    assert format_line("foo{foo}foo", data) == "foobar bazfoo"


def test_format_line_erroneous():
    data = dict()
    with pytest.raises(PlaceholderError):
        assert format_line("{invalid}", data)
    with pytest.raises(PlaceholderError):
        assert format_line("{}", data)
    with pytest.raises(PlaceholderError):
        assert format_line("{now!r}", data)
    with pytest.raises(PlaceholderError):
        assert format_line("{now.__class__.__module__.__builtins__}", data)


def test_replace_placeholders():
    replace_placeholders.reset()  # avoid overrides are spoiled by previous tests
    now = datetime.now()
    assert " " not in replace_placeholders("{now}")
    assert int(replace_placeholders("{now:%Y}")) == now.year


def test_override_placeholders():
    assert replace_placeholders("{uuid4}", overrides={"uuid4": "overridden"}) == "overridden"


def working_swidth():
    from ...platform import swidth

    return swidth("선") == 2


@pytest.mark.skipif(not working_swidth(), reason="swidth() is not supported / active")
def test_swidth_slice():
    string = "나윤선나윤선나윤선나윤선나윤선"
    assert swidth_slice(string, 1) == ""
    assert swidth_slice(string, -1) == ""
    assert swidth_slice(string, 4) == "나윤"
    assert swidth_slice(string, -4) == "윤선"


@pytest.mark.skipif(not working_swidth(), reason="swidth() is not supported / active")
def test_swidth_slice_mixed_characters():
    string = "나윤a선나윤선나윤선나윤선나윤선"
    assert swidth_slice(string, 5) == "나윤a"
    assert swidth_slice(string, 6) == "나윤a"


def test_eval_escapes():
    assert eval_escapes("\\n") == "\n"
    assert eval_escapes("\\t") == "\t"
    assert eval_escapes("\\r") == "\r"
    assert eval_escapes("\\f") == "\f"
    assert eval_escapes("\\b") == "\b"
    assert eval_escapes("\\a") == "\a"
    assert eval_escapes("\\v") == "\v"
    assert eval_escapes("\\\\") == "\\"
    assert eval_escapes('\\"') == '"'
    assert eval_escapes("\\'") == "'"
    assert eval_escapes("\\101") == "A"  # ord('A') == 65 == 0o101
    assert eval_escapes("\\x41") == "A"  # ord('A') == 65 == 0x41
    assert eval_escapes("\\u0041") == "A"  # ord('A') == 65 == 0x41
    assert eval_escapes("\\U00000041") == "A"  # ord('A') == 65 == 0x41
    assert eval_escapes("äç\\n") == "äç\n"


@pytest.mark.parametrize(
    "chunker_params, expected_return",
    [
        ("default", ("buzhash", 19, 23, 21, 4095)),
        ("19,23,21,4095", ("buzhash", 19, 23, 21, 4095)),
        ("buzhash,19,23,21,4095", ("buzhash", 19, 23, 21, 4095)),
        ("10,23,16,4095", ("buzhash", 10, 23, 16, 4095)),
        ("fixed,4096", ("fixed", 4096, 0)),
        ("fixed,4096,200", ("fixed", 4096, 200)),
    ],
)
def test_valid_chunkerparams(chunker_params, expected_return):
    assert ChunkerParams(chunker_params) == expected_return


@pytest.mark.parametrize(
    "invalid_chunker_params",
    [
        "crap,1,2,3,4",  # invalid algo
        "buzhash,5,7,6,4095",  # too small min. size
        "buzhash,19,24,21,4095",  # too big max. size
        "buzhash,23,19,21,4095",  # violates min <= mask <= max
        "buzhash,19,23,21,4096",  # even window size
        "buzhash,19,23,21",  # missing window_size (must not fall into old-style compat mode)
        "buzhash64,20,20,20,4095,2",  # window_size + 2^chunk_min + 1 > 2^chunk_max
        "buzhash64,19,19,19,4095,2",  # dito, chunk_min == chunk_max
        "buzhash64,19,23,21,4095",  # missing nc_level
        "fastcdc,20,20,20,2",  # chunk_min == chunk_max
        "fastcdc,19,23,21",  # missing nc_level (must not fall into old-style compat mode)
        "fixed,63",  # too small block size
        "fixed,%d,%d" % (MAX_DATA_SIZE + 1, 4096),  # too big block size
        "fixed,%d,%d" % (4096, MAX_DATA_SIZE + 1),  # too big header size
    ],
)
def test_invalid_chunkerparams(invalid_chunker_params):
    with pytest.raises(ArgumentTypeError):
        ChunkerParams(invalid_chunker_params)


@pytest.mark.parametrize(
    "env_value, expect_newlines",
    [
        (None, True),  # default indent=4
        ("none", False),  # compact single-line
        ("0", True),  # newlines only, no spaces
        ("4", True),  # explicit pretty-print
        ("", True),  # empty string, newlines only
        ("\t", True),  # tab indent string
    ],
)
def test_json_dump_indent(monkeypatch, env_value, expect_newlines):
    from ...helpers.parseformat import json_dump

    obj = {"key": "value", "number": 42}
    if env_value is not None:
        monkeypatch.setenv("BORG_JSON_INDENT", env_value)
    else:
        monkeypatch.delenv("BORG_JSON_INDENT", raising=False)

    result = json_dump(obj)
    if expect_newlines:
        assert "\n" in result
    else:
        assert "\n" not in result
    assert json.loads(result) == obj
