# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========

import pytest

from mirage.commands.builtin.generic.cp import cp
from mirage.types import (FileStat, FileType, NativeCopy, PathSpec,
                          PrimitiveCopy)
from mirage.utils.errors import enotsup


def _spec(path: str) -> PathSpec:
    return PathSpec(virtual=path,
                    directory=path,
                    resource_path=path.strip("/"))


def _key(p) -> str:
    return (p.virtual if isinstance(p, PathSpec) else p).rstrip("/")


def _make_backend(files: dict[str, bytes], dirs: set[str]):

    async def stat(p) -> FileStat:
        k = _key(p)
        if k in dirs:
            return FileStat(name=k.rsplit("/", 1)[-1], type=FileType.DIRECTORY)
        if k in files:
            return FileStat(name=k.rsplit("/", 1)[-1], type=FileType.TEXT)
        raise FileNotFoundError(k)

    async def copy(src, dst) -> None:
        files[_key(dst)] = files[_key(src)]

    async def find(p, type=None) -> list[str]:
        base = _key(p) + "/"
        return sorted(k for k in files if k.startswith(base))

    return stat, copy, find


async def _run(files, dirs, paths, **kw):
    stat, copy, find = _make_backend(files, dirs)
    return await cp([_spec(p) for p in paths],
                    strategy=NativeCopy(copy=copy, find=find),
                    find_type="f",
                    stat=stat,
                    recursive=kw.get("recursive", False),
                    n=kw.get("n", False),
                    v=kw.get("v", False))


@pytest.mark.asyncio
async def test_single_source_to_new_path():
    files = {"/a.txt": b"AAA"}
    await _run(files, set(), ["/a.txt", "/copy.txt"])
    assert files["/copy.txt"] == b"AAA"


@pytest.mark.asyncio
async def test_single_source_into_directory():
    files = {"/a.txt": b"AAA", "/d/keep": b"K"}
    await _run(files, {"/d"}, ["/a.txt", "/d"])
    assert files["/d/a.txt"] == b"AAA"
    assert files["/a.txt"] == b"AAA"


@pytest.mark.asyncio
async def test_multiple_sources_into_directory():
    files = {"/a.txt": b"AAA", "/b.txt": b"BBB", "/d/keep": b"K"}
    await _run(files, {"/d"}, ["/a.txt", "/b.txt", "/d"])
    assert files["/d/a.txt"] == b"AAA"
    assert files["/d/b.txt"] == b"BBB"


@pytest.mark.asyncio
async def test_multiple_sources_nondir_raises():
    files = {"/a.txt": b"AAA", "/b.txt": b"BBB", "/dst.txt": b"DST"}
    with pytest.raises(NotADirectoryError):
        await _run(files, set(), ["/a.txt", "/b.txt", "/dst.txt"])
    assert files["/dst.txt"] == b"DST"


@pytest.mark.asyncio
async def test_no_clobber_skips_existing():
    files = {"/a.txt": b"NEW", "/d/a.txt": b"OLD"}
    await _run(files, {"/d"}, ["/a.txt", "/d"], n=True)
    assert files["/d/a.txt"] == b"OLD"


@pytest.mark.asyncio
async def test_no_clobber_duplicate_basenames_first_wins():
    files = {"/x/a.txt": b"FIRST", "/y/a.txt": b"SECOND", "/d/keep": b"K"}
    await _run(files, {"/d"}, ["/x/a.txt", "/y/a.txt", "/d"], n=True)
    assert files["/d/a.txt"] == b"FIRST"


@pytest.mark.asyncio
async def test_duplicate_basenames_without_n_last_wins():
    files = {"/x/a.txt": b"FIRST", "/y/a.txt": b"SECOND", "/d/keep": b"K"}
    await _run(files, {"/d"}, ["/x/a.txt", "/y/a.txt", "/d"])
    assert files["/d/a.txt"] == b"SECOND"


@pytest.mark.asyncio
async def test_recursive_into_directory():
    files = {"/src/x.txt": b"X", "/src/sub/y.txt": b"Y"}
    await _run(files, {"/src"}, ["/src", "/dst"], recursive=True)
    assert files["/dst/x.txt"] == b"X"
    assert files["/dst/sub/y.txt"] == b"Y"


@pytest.mark.asyncio
async def test_verbose_emits_arrow_lines():
    files = {"/a.txt": b"AAA"}
    out, _ = await _run(files, set(), ["/a.txt", "/copy.txt"], v=True)
    assert out == b"'/a.txt' -> '/copy.txt'\n"


@pytest.mark.asyncio
async def test_records_writes_by_strip_prefix():
    files = {"/a.txt": b"AAA", "/b.txt": b"BBB", "/d/keep": b"K"}
    _, io = await _run(files, {"/d"}, ["/a.txt", "/b.txt", "/d"])
    assert set(io.writes) == {"/d/a.txt", "/d/b.txt"}


@pytest.mark.asyncio
async def test_missing_source_reports_cannot_stat_and_continues():
    files = {"/b.txt": b"BBB", "/d/keep": b"K"}
    _, io = await _run(files, {"/d"}, ["/missing.txt", "/b.txt", "/d"])
    assert io.exit_code == 1
    assert b"cp: cannot stat '/missing.txt'" in io.stderr
    assert files["/d/b.txt"] == b"BBB"


@pytest.mark.asyncio
async def test_same_file_errors_and_preserves_content():
    files = {"/a.txt": b"AAA"}
    _, io = await _run(files, set(), ["/a.txt", "/a.txt"])
    assert io.exit_code == 1
    assert b"'/a.txt' and '/a.txt' are the same file" in io.stderr
    assert files["/a.txt"] == b"AAA"


@pytest.mark.asyncio
async def test_same_file_via_directory_target_errors():
    files = {"/d/a.txt": b"AAA", "/d/keep": b"K"}
    _, io = await _run(files, {"/d"}, ["/d/a.txt", "/d"])
    assert io.exit_code == 1
    assert b"are the same file" in io.stderr
    assert files["/d/a.txt"] == b"AAA"


@pytest.mark.asyncio
async def test_recursive_into_own_subtree_refused():
    files = {"/d/a.txt": b"AAA"}
    _, io = await _run(files, {"/d"}, ["/d", "/d"], recursive=True)
    assert io.exit_code == 1
    assert b"cp: cannot copy a directory, '/d', into itself" in io.stderr
    assert set(files) == {"/d/a.txt"}


@pytest.mark.asyncio
async def test_recursive_into_nested_subtree_refused():
    files = {"/d/a.txt": b"AAA"}
    _, io = await _run(files, {"/d", "/d/sub"}, ["/d", "/d/sub"],
                       recursive=True)
    assert io.exit_code == 1
    assert b"into itself" in io.stderr
    assert set(files) == {"/d/a.txt"}


@pytest.mark.asyncio
async def test_primitive_copy_records_source_reads():
    files = {"/a.txt": b"AAA"}
    stat, _, _ = _make_backend(files, set())

    async def read_bytes(p) -> bytes:
        return files[_key(p)]

    async def write(p, data: bytes) -> None:
        files[_key(p)] = data

    _, io = await cp([_spec("/a.txt"), _spec("/copy.txt")],
                     stat=stat,
                     strategy=PrimitiveCopy(read_bytes=read_bytes,
                                            write=write,
                                            mkdir=write,
                                            readdir=write),
                     recursive=False,
                     n=False,
                     v=False)
    assert files["/copy.txt"] == b"AAA"
    assert io.reads == {"/a.txt": b"AAA"}
    assert io.cache == ["/a.txt"]


@pytest.mark.asyncio
async def test_native_copy_records_no_reads():
    files = {"/a.txt": b"AAA"}
    _, io = await _run(files, set(), ["/a.txt", "/copy.txt"])
    assert io.reads == {}
    assert io.cache == []


def _make_primitive(files: dict[str, bytes],
                    dirs: set[str],
                    *,
                    read_fails: dict | None = None,
                    write_fails: dict | None = None):
    stat, _, _ = _make_backend(files, dirs)
    read_err = read_fails or {}
    write_err = write_fails or {}

    async def read_bytes(p) -> bytes:
        if _key(p) in read_err:
            raise read_err[_key(p)]
        return files[_key(p)]

    async def write(p, data: bytes) -> None:
        if _key(p) in write_err:
            raise write_err[_key(p)]
        files[_key(p)] = data

    async def mkdir(p) -> None:
        dirs.add(_key(p))

    async def readdir(p) -> list[str]:
        base = _key(p) + "/"
        children = {
            base + k[len(base):].split("/", 1)[0]
            for k in set(files) | dirs if k.startswith(base)
        }
        return sorted(children)

    strategy = PrimitiveCopy(read_bytes=read_bytes,
                             write=write,
                             mkdir=mkdir,
                             readdir=readdir)
    return stat, strategy


async def _run_primitive(files, dirs, paths, *, recursive=False, **fail_kw):
    stat, strategy = _make_primitive(files, dirs, **fail_kw)
    return await cp([_spec(p) for p in paths],
                    strategy=strategy,
                    stat=stat,
                    recursive=recursive,
                    n=False,
                    v=False)


@pytest.mark.asyncio
async def test_primitive_read_failure_reports_cannot_open():
    files = {"/src/a.txt": b"AAA", "/src/b.txt": b"BBB", "/d/keep": b"K"}
    _, io = await _run_primitive(
        files, {"/src", "/d"}, ["/src/a.txt", "/src/b.txt", "/d"],
        read_fails={"/src/a.txt": PermissionError("/src/a.txt")})
    assert io.exit_code == 1
    assert io.stderr == (b"cp: cannot open '/src/a.txt' for reading: "
                         b"Permission denied\n")
    assert "/d/a.txt" not in files
    assert files["/d/b.txt"] == b"BBB"


@pytest.mark.asyncio
async def test_primitive_write_failure_reports_cannot_create():
    files = {"/src/a.txt": b"AAA", "/d/keep": b"K"}
    _, io = await _run_primitive(
        files, {"/src", "/d"}, ["/src/a.txt", "/d"],
        write_fails={"/d/a.txt": enotsup("notion", "write", "/d/a.txt")})
    assert io.exit_code == 1
    assert io.stderr == (b"cp: cannot create regular file '/d/a.txt': "
                         b"Operation not supported\n")
    assert files["/src/a.txt"] == b"AAA"
    assert io.reads == {}


@pytest.mark.asyncio
async def test_primitive_recursive_read_failure_copies_rest():
    files = {"/src/t/a.txt": b"A", "/src/t/nr.txt": b"NR"}
    dirs = {"/src", "/src/t", "/d"}
    _, io = await _run_primitive(
        files,
        dirs, ["/src/t", "/d/t"],
        recursive=True,
        read_fails={"/src/t/nr.txt": PermissionError("/src/t/nr.txt")})
    assert io.exit_code == 1
    assert io.stderr == (b"cp: cannot open '/src/t/nr.txt' for reading: "
                         b"Permission denied\n")
    assert files["/d/t/a.txt"] == b"A"
    assert "/d/t/nr.txt" not in files
