import pytest

from . import Archiver, RK_ENCRYPTION, cmd
from ...helpers import PathNotAllowed
from ...helpers.argparsing import ArgumentParser, flatten_namespace


def test_bad_filters(archiver):
    cmd(archiver, "repo-create", RK_ENCRYPTION)
    cmd(archiver, "create", "test", "input")
    cmd(archiver, "delete", "--first", "1", "--last", "1", fork=True, exit_code=2)


def test_highlander(archiver):
    cmd(archiver, "repo-create", RK_ENCRYPTION)
    cmd(archiver, "create", "--comment", "comment 1", "test-1", __file__)
    error_msg = "There can be only one"
    # Default umask value is 0077
    # Test that it works with a one-time specified default or custom value
    output_default = cmd(archiver, "--umask", "0077", "repo-list")
    assert error_msg not in output_default
    output_custom = cmd(archiver, "--umask", "0007", "repo-list")
    assert error_msg not in output_custom
    # Test that all combinations of custom and default values fail
    for first, second in [("0007", "0007"), ("0007", "0077"), ("0077", "0007"), ("0077", "0077")]:
        output_custom = cmd(archiver, "--umask", first, "--umask", second, "repo-list", exit_code=2)
        assert error_msg in output_custom


def test_get_args():
    archiver = Archiver()
    # everything normal:
    # first param is argv as produced by ssh forced command,
    # second param is like from SSH_ORIGINAL_COMMAND env variable
    args = archiver.get_args(
        ["borg", "serve", "--umask=0027", "--restrict-to-path=/p1", "--restrict-to-path=/p2"], "borg serve --info"
    )
    assert args.func == archiver.do_serve
    assert args.restrict_to_paths == ["/p1", "/p2"]
    assert args.umask == 0o027
    assert args.log_level == "info"
    # similar, but with --restrict-to-repository
    args = archiver.get_args(
        ["borg", "serve", "--restrict-to-repository=/r1", "--restrict-to-repository=/r2"],
        "borg serve --info --umask=0027",
    )
    assert args.restrict_to_repositories == ["/r1", "/r2"]
    # trying to cheat - break out of path restriction
    args = archiver.get_args(
        ["borg", "serve", "--restrict-to-path=/p1", "--restrict-to-path=/p2"], "borg serve --restrict-to-path=/"
    )
    assert args.restrict_to_paths == ["/p1", "/p2"]
    # trying to cheat - break out of repository restriction
    args = archiver.get_args(
        ["borg", "serve", "--restrict-to-repository=/r1", "--restrict-to-repository=/r2"],
        "borg serve --restrict-to-repository=/",
    )
    assert args.restrict_to_repositories == ["/r1", "/r2"]
    # trying to cheat - break below repository restriction
    args = archiver.get_args(
        ["borg", "serve", "--restrict-to-repository=/r1", "--restrict-to-repository=/r2"],
        "borg serve --restrict-to-repository=/r1/below",
    )
    assert args.restrict_to_repositories == ["/r1", "/r2"]
    # trying to cheat - try to execute different subcommand
    args = archiver.get_args(
        ["borg", "serve", "--restrict-to-path=/p1", "--restrict-to-path=/p2"],
        f"borg --repo=/ repo-create {RK_ENCRYPTION}",
    )
    assert args.func == archiver.do_serve

    # Check that environment variables in the forced command don't cause issues. If the command
    # were not forced, environment variables would be interpreted by the shell, but this does not
    # happen for forced commands - we get the verbatim command line and need to deal with env vars.
    args = archiver.get_args(["borg", "serve"], "BORG_FOO=bar borg serve --info")
    assert args.func == archiver.do_serve

    # rest server: the client chooses the backend (which repo), the forced command pins the
    # restriction and the rest mode. The client's --backend must survive the merge so that
    # do_serve_rest can validate it against the forced restrictions.
    args = archiver.get_args(
        ["borg", "serve", "--rest", "--restrict-to-path=/p1"], "borg serve --rest --backend=FILE:/p1/myrepo"
    )
    assert args.func == archiver.do_serve
    assert args.rest is True
    assert args.restrict_to_paths == ["/p1"]
    assert args.backend == "FILE:/p1/myrepo"
    # the forced command pins the mode: a client cannot turn a forced legacy serve into a rest serve
    args = archiver.get_args(["borg", "serve"], "borg serve --rest --backend=FILE:/p1/myrepo")
    assert args.rest is False


def test_check_rest_restrictions(tmp_path):
    archiver = Archiver()
    check = archiver.check_rest_restrictions
    allowed = tmp_path / "allowed"
    allowed.mkdir()
    repo = allowed / "myrepo"

    # no restrictions: anything is allowed
    check(f"FILE:{repo}", None, None)

    # restrict-to-path: a backend below the allowed path is fine, outside is rejected
    check(f"FILE:{repo}", [str(allowed)], None)
    with pytest.raises(PathNotAllowed):
        check(f"FILE:{tmp_path / 'other' / 'repo'}", [str(allowed)], None)

    # restrict-to-repository: only the exact repo path is allowed
    check(f"FILE:{repo}", None, [str(repo)])
    with pytest.raises(PathNotAllowed):
        check(f"FILE:{allowed / 'evil'}", None, [str(repo)])

    # a non-FILE: backend cannot be restricted
    with pytest.raises(PathNotAllowed):
        check("sftp://host/path", [str(allowed)], None)


class TestCommonOptions:
    @staticmethod
    def define_common_options(add_common_option):
        add_common_option("-h", "--help", action="help", help="show this help message and exit")
        add_common_option(
            "--critical", dest="log_level", help="foo", action="store_const", const="critical", default="warning"
        )
        add_common_option(
            "--error", dest="log_level", help="foo", action="store_const", const="error", default="warning"
        )
        add_common_option("--append", dest="append", help="foo", action="append", metavar="TOPIC", default=[])
        add_common_option("-p", "--progress", dest="progress", action="store_true", help="foo")
        add_common_option(
            "--lock-wait", dest="lock_wait", type=int, metavar="N", default=1, help="(default: %(default)d)."
        )

    @pytest.fixture
    def basic_parser(self):
        parser = ArgumentParser(prog="test", description="test parser")
        parser.common_options = Archiver.CommonOptions(self.define_common_options)
        return parser

    @pytest.fixture
    def subcommands(self, basic_parser):
        return basic_parser.add_subcommands(required=False, title="required arguments", metavar="<command>")

    @pytest.fixture
    def parser(self, basic_parser):
        basic_parser.common_options.add_common_group(basic_parser, provide_defaults=True)
        return basic_parser

    @pytest.fixture
    def common_parser(self, parser):
        common_parser = ArgumentParser(prog="test")
        parser.common_options.add_common_group(common_parser)
        return common_parser

    @pytest.fixture
    def parse_vars_from_line(self, parser, subcommands, common_parser):
        subparser = ArgumentParser(parents=[common_parser], description="foo", epilog="bar")
        subparser.add_argument("--foo-bar", dest="foo_bar", action="store_true")
        subcommands.add_subcommand("subcmd", subparser, help="baz")

        def parse_vars_from_line(*line):
            print(line)
            args = parser.parse_args(line)
            args = flatten_namespace(args)
            return vars(args)

        return parse_vars_from_line

    def test_simple(self, parse_vars_from_line):
        assert parse_vars_from_line("--error") == {
            "append": [],
            "lock_wait": 1,
            "log_level": "error",
            "progress": False,
        }

        assert parse_vars_from_line("--error", "subcmd", "--critical") == {
            "append": [],
            "lock_wait": 1,
            "log_level": "critical",
            "progress": False,
            "foo_bar": False,
            "subcommand": "subcmd",
        }

        with pytest.raises(SystemExit):
            parse_vars_from_line("--foo-bar", "subcmd")

        assert parse_vars_from_line("--append=foo", "--append", "bar", "subcmd", "--append", "baz") == {
            "append": ["foo", "bar", "baz"],
            "lock_wait": 1,
            "log_level": "warning",
            "progress": False,
            "foo_bar": False,
            "subcommand": "subcmd",
        }

    @pytest.mark.parametrize("position", ("before", "after", "both"))
    @pytest.mark.parametrize("flag,args_key,args_value", (("-p", "progress", True), ("--lock-wait=3", "lock_wait", 3)))
    def test_flag_position_independence(self, parse_vars_from_line, position, flag, args_key, args_value):
        line = []
        if position in ("before", "both"):
            line.append(flag)
        line.append("subcmd")
        if position in ("after", "both"):
            line.append(flag)

        result = {
            "append": [],
            "lock_wait": 1,
            "log_level": "warning",
            "progress": False,
            "foo_bar": False,
            "subcommand": "subcmd",
            args_key: args_value,
        }

        assert parse_vars_from_line(*line) == result
