from contextlib import nullcontext as does_not_raise
from typing import ContextManager, Union

import pytest
import yaml
from jinja2.ext import Extension
from plumbum import local
from plumbum.cmd import git

from copier.cli import CopierApp
from copier.errors import UnsafeTemplateError
from copier.main import run_copy, run_update
from copier.types import AnyByStrDict

from .helpers import build_file_tree


class JinjaExtension(Extension):
    ...


@pytest.mark.parametrize(
    ("spec", "expected"),
    [
        (
            {},
            does_not_raise(),
        ),
        (
            {"_tasks": []},
            does_not_raise(),
        ),
        (
            {"_tasks": ["touch task.txt"]},
            pytest.raises(
                UnsafeTemplateError, match="Template uses unsafe feature: tasks"
            ),
        ),
        (
            {"_migrations": []},
            does_not_raise(),
        ),
        (
            {
                "_migrations": [
                    {
                        "version": "v1",
                        "before": ["touch v1-before.txt"],
                        "after": ["touch v1-after.txt"],
                    }
                ]
            },
            does_not_raise(),
        ),
        (
            {"_jinja_extensions": []},
            does_not_raise(),
        ),
        (
            {"_jinja_extensions": ["tests.test_unsafe.JinjaExtension"]},
            pytest.raises(
                UnsafeTemplateError,
                match="Template uses unsafe feature: jinja_extensions",
            ),
        ),
        (
            {
                "_tasks": ["touch task.txt"],
                "_jinja_extensions": ["tests.test_unsafe.JinjaExtension"],
            },
            pytest.raises(
                UnsafeTemplateError,
                match="Template uses unsafe features: jinja_extensions, tasks",
            ),
        ),
    ],
)
@pytest.mark.parametrize("unsafe", [False, True])
def test_copy(
    tmp_path_factory: pytest.TempPathFactory,
    unsafe: bool,
    spec: AnyByStrDict,
    expected: ContextManager[None],
) -> None:
    src, dst = map(tmp_path_factory.mktemp, ["src", "dst"])
    build_file_tree({(src / "copier.yaml"): yaml.safe_dump(spec)})
    with does_not_raise() if unsafe else expected:
        run_copy(str(src), dst, unsafe=unsafe)


@pytest.mark.parametrize("unsafe", [False, True])
def test_copy_cli(
    tmp_path_factory: pytest.TempPathFactory,
    capsys: pytest.CaptureFixture[str],
    unsafe: bool,
) -> None:
    src, dst = map(tmp_path_factory.mktemp, ["src", "dst"])
    build_file_tree(
        {(src / "copier.yaml"): yaml.safe_dump({"_tasks": ["touch task.txt"]})}
    )
    _, retcode = CopierApp.run(
        ["copier", "copy", *(["--UNSAFE"] if unsafe else []), str(src), str(dst)],
        exit=False,
    )
    if unsafe:
        assert retcode == 0
    else:
        assert retcode == 2
        _, err = capsys.readouterr()
        assert "Template uses unsafe feature: tasks" in err


@pytest.mark.parametrize(
    ("spec_old", "spec_new", "expected"),
    [
        (
            {},
            {},
            does_not_raise(),
        ),
        (
            {"_tasks": []},
            {"_tasks": []},
            does_not_raise(),
        ),
        (
            {"_tasks": ["touch task-old.txt"]},
            {},
            pytest.raises(
                UnsafeTemplateError, match="Template uses unsafe feature: tasks"
            ),
        ),
        (
            {},
            {"_tasks": ["touch task-new.txt"]},
            pytest.raises(
                UnsafeTemplateError, match="Template uses unsafe feature: tasks"
            ),
        ),
        (
            {"_tasks": ["touch task-old.txt"]},
            {"_tasks": ["touch task-new.txt"]},
            pytest.raises(
                UnsafeTemplateError, match="Template uses unsafe feature: tasks"
            ),
        ),
        (
            {"_migrations": []},
            {"_migrations": []},
            does_not_raise(),
        ),
        (
            {},
            {
                "_migrations": [
                    {
                        "version": "v0",
                        "before": ["touch v0-before.txt"],
                        "after": ["touch v0-after.txt"],
                    }
                ]
            },
            does_not_raise(),
        ),
        (
            {},
            {
                "_migrations": [
                    {
                        "version": "v2",
                        "before": [],
                        "after": [],
                    }
                ]
            },
            does_not_raise(),
        ),
        (
            {},
            {
                "_migrations": [
                    {
                        "version": "v2",
                        "before": ["touch v2-before.txt"],
                    }
                ]
            },
            pytest.raises(
                UnsafeTemplateError, match="Template uses unsafe feature: migrations"
            ),
        ),
        (
            {},
            {
                "_migrations": [
                    {
                        "version": "v2",
                        "after": ["touch v2-after.txt"],
                    }
                ]
            },
            pytest.raises(
                UnsafeTemplateError, match="Template uses unsafe feature: migrations"
            ),
        ),
        (
            {},
            {
                "_migrations": [
                    {
                        "version": "v2",
                        "before": ["touch v2-before.txt"],
                        "after": ["touch v2-after.txt"],
                    }
                ]
            },
            pytest.raises(
                UnsafeTemplateError, match="Template uses unsafe feature: migrations"
            ),
        ),
        (
            {"_jinja_extensions": []},
            {"_jinja_extensions": []},
            does_not_raise(),
        ),
        (
            {"_jinja_extensions": ["tests.test_unsafe.JinjaExtension"]},
            {},
            pytest.raises(
                UnsafeTemplateError,
                match="Template uses unsafe feature: jinja_extensions",
            ),
        ),
        (
            {},
            {"_jinja_extensions": ["tests.test_unsafe.JinjaExtension"]},
            pytest.raises(
                UnsafeTemplateError,
                match="Template uses unsafe feature: jinja_extensions",
            ),
        ),
        (
            {"_jinja_extensions": ["tests.test_unsafe.JinjaExtension"]},
            {"_jinja_extensions": ["tests.test_unsafe.JinjaExtension"]},
            pytest.raises(
                UnsafeTemplateError,
                match="Template uses unsafe feature: jinja_extensions",
            ),
        ),
        (
            {},
            {
                "_tasks": ["touch task-new.txt"],
                "_migrations": [
                    {
                        "version": "v2",
                        "before": ["touch v2-before.txt"],
                        "after": ["touch v2-after.txt"],
                    }
                ],
                "_jinja_extensions": ["tests.test_unsafe.JinjaExtension"],
            },
            pytest.raises(
                UnsafeTemplateError,
                match=(
                    "Template uses unsafe features: "
                    "jinja_extensions, migrations, tasks"
                ),
            ),
        ),
    ],
)
@pytest.mark.parametrize("unsafe", [False, True])
def test_update(
    tmp_path_factory: pytest.TempPathFactory,
    unsafe: bool,
    spec_old: AnyByStrDict,
    spec_new: AnyByStrDict,
    expected: ContextManager[None],
) -> None:
    src, dst = map(tmp_path_factory.mktemp, ["src", "dst"])

    with local.cwd(src):
        build_file_tree(
            {
                "{{ _copier_conf.answers_file }}.jinja": "{{ _copier_answers | to_nice_yaml }}",
                "copier.yaml": yaml.safe_dump(spec_old),
            }
        )
        git("init")
        git("add", ".")
        git("commit", "-m1")
        git("tag", "v1")

    run_copy(str(src), dst, unsafe=True)
    assert "_commit: v1" in (dst / ".copier-answers.yml").read_text()

    with local.cwd(dst):
        git("init")
        git("add", ".")
        git("commit", "-m1")

    with local.cwd(src):
        build_file_tree(
            {
                "copier.yaml": yaml.safe_dump(spec_new),
            }
        )
        git("add", ".")
        git("commit", "-m2", "--allow-empty")
        git("tag", "v2")

    with does_not_raise() if unsafe else expected:
        run_update(dst, overwrite=True, unsafe=unsafe)


@pytest.mark.parametrize("unsafe", [False, "--trust", "--UNSAFE"])
def test_update_cli(
    tmp_path_factory: pytest.TempPathFactory,
    capsys: pytest.CaptureFixture[str],
    unsafe: Union[bool, str],
) -> None:
    src, dst = map(tmp_path_factory.mktemp, ["src", "dst"])
    unsafe_args = [unsafe] if unsafe else []

    with local.cwd(src):
        build_file_tree(
            {
                "{{ _copier_conf.answers_file }}.jinja": "{{ _copier_answers | to_nice_yaml }}",
                "copier.yaml": "",
            }
        )
        git("init")
        git("add", ".")
        git("commit", "-m1")
        git("tag", "v1")

    _, retcode = CopierApp.run(
        ["copier", "copy", str(src), str(dst)] + unsafe_args,
        exit=False,
    )
    assert retcode == 0
    assert "_commit: v1" in (dst / ".copier-answers.yml").read_text()
    capsys.readouterr()

    with local.cwd(dst):
        git("init")
        git("add", ".")
        git("commit", "-m1")

    with local.cwd(src):
        build_file_tree(
            {
                "copier.yaml": yaml.safe_dump({"_tasks": ["touch task-new.txt"]}),
            }
        )
        git("add", ".")
        git("commit", "-m2")
        git("tag", "v2")

    _, retcode = CopierApp.run(
        [
            "copier",
            "update",
            str(dst),
        ]
        + unsafe_args,
        exit=False,
    )
    if unsafe:
        assert retcode == 0
    else:
        assert retcode == 2
        _, err = capsys.readouterr()
        assert "Template uses unsafe feature: tasks" in err
