"""Verify implementation of RPC file helpers."""

import collections.abc
import contextlib
import datetime
import pathlib
import typing

import pytest

from shv import SHVType
from shv.rpcapi import SHVBase
from shv.rpcapi.client import SHVClient
from shv.rpcdef.file import FileProviderRW, RpcAccess, RpcDir, RpcFile, RpcFileStat


class FilesProvider(SHVClient):
    APP_NAME = "testdev"

    def __init__(
        self,
        *args: typing.Any,  # noqa ANNN401
        path: pathlib.Path,
        **kwargs: typing.Any,  # noqa ANNN401
    ) -> None:
        self.path = path
        super().__init__(*args, **kwargs)  # notype

    def _ls(self, path: str) -> collections.abc.Iterator[str]:
        yield from super()._ls(path)
        if not path:
            yield "files"
        elif path == "files" or path.startswith("files/"):
            pth = self.path / path[6:]
            if pth.is_dir():
                yield from sorted(spth.name for spth in pth.iterdir())

    def _dir(self, path: str) -> collections.abc.Iterator[RpcDir]:
        yield from super()._dir(path)
        if path.startswith("files/"):
            pth = self.path / path[6:]
            if pth.is_file():
                yield from FileProviderRW.dir()

    async def _method_call(self, request: SHVBase.Request) -> SHVType:
        if request.path.startswith("files/"):
            pth = self.path / request.path[6:]
            if pth.is_file():
                with contextlib.suppress(NotImplementedError):
                    # TODO use also other providers
                    return await FileProviderRW.method_call(pth, request)
        return await super()._method_call(request)


@pytest.fixture(name="device")
async def fixture_device(shvbroker, url_test_device, tmp_path):
    """Run FilesProvider and provide instance to access it."""
    device = await FilesProvider.connect(url_test_device, path=tmp_path)
    yield device
    await device.disconnect()


@pytest.fixture(name="seed_tmp")
def fixture_seed_tmp(tmp_path):
    """Create some dummy files in tmp_path."""
    (tmp_path / "bin").write_bytes(b"abc")
    (tmp_path / "lorem.txt").write_text(
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
    )
    (tmp_path / "sub" / "subsub").mkdir(parents=True)


@pytest.mark.parametrize(
    "path,res",
    (
        ("test/device", [".app", "files"]),
        ("test/device/files", ["bin", "lorem.txt", "sub"]),
        ("test/device/files/bin", []),
        ("test/device/files/lorem.txt", []),
        ("test/device/files/sub", ["subsub"]),
        ("test/device/files/sub/subsub", []),
    ),
)
async def test_ls(client, device, seed_tmp, path, res):
    assert await client.ls(path) == res


@pytest.mark.parametrize(
    "path,res",
    (
        (
            "test/device/files/lorem.txt",
            [
                RpcDir.stddir(),
                RpcDir.stdls(),
                RpcDir.getter("stat", "n", "!stat", RpcAccess.READ),
                RpcDir.getter("size", "n", "i(0,)", RpcAccess.READ),
                RpcDir(
                    "crc",
                    param="[i(0,):offset,i(0,)|n:size]|n",
                    result="u(>32)",
                    access=RpcAccess.READ,
                ),
                RpcDir(
                    "sha1",
                    param="[i(0,):offset,i(0,)|n:size]|n",
                    result="b(20)",
                    access=RpcAccess.READ,
                ),
                RpcDir(
                    "read",
                    RpcDir.Flag.LARGE_RESULT_HINT,
                    "[i(0,):offset,i(0,)size]",
                    "b",
                    RpcAccess.READ,
                ),
                RpcDir(
                    "write",
                    param="[i(0,):offset,b:data]",
                    result="n",
                    access=RpcAccess.WRITE,
                ),
                RpcDir("truncate", param="i(0,)", result="n", access=RpcAccess.WRITE),
                RpcDir("append", param="b", result="n", access=RpcAccess.WRITE),
            ],
        ),
    ),
)
async def test_dir(client, device, seed_tmp, path, res):
    assert await client.dir(path) == res


@pytest.fixture(name="file_bin")
def fixture_file_bin(client, device, seed_tmp):
    return RpcFile(client, "test/device/files/bin", RpcFile.Flag(0))


def test_file_bin_path(file_bin):
    assert file_bin.path == "test/device/files/bin"


async def test_file_bin_readable(file_bin):
    assert await file_bin.readable()


async def test_file_bin_writable(file_bin):
    assert await file_bin.writable()


async def test_file_bin_resizable(file_bin):
    assert await file_bin.resizable()


async def test_file_bin_stat(file_bin, tmp_path):
    stat = (tmp_path / "bin").stat()
    access = datetime.datetime.fromtimestamp(stat.st_atime, tz=datetime.UTC)
    access = access.replace(microsecond=(access.microsecond // 1000) * 1000)
    mod = datetime.datetime.fromtimestamp(stat.st_mtime, tz=datetime.UTC)
    mod = access.replace(microsecond=(mod.microsecond // 1000) * 1000)
    assert await file_bin.stat() == RpcFileStat(
        0, stat.st_size, stat.st_blksize, access, mod
    )


async def test_file_bin_size(client, device, seed_tmp):
    assert await client.call("test/device/files/bin", "size") == 3


@pytest.mark.parametrize(
    "offset,size,res",
    (
        (0, None, 891568578),
        (1, None, 3265866552),
        (0, 1, 3904355907),
    ),
)
async def test_file_bin_crc32(file_bin, offset, size, res):
    assert await file_bin.crc32(offset, size) == res


@pytest.mark.parametrize(
    "offset,size,res",
    (
        (0, None, b"\xa9\x99>6G\x06\x81j\xba>%qxP\xc2l\x9c\xd0\xd8\x9d"),
        (1, None, b"[%\x05\x03\x9a\xc5\xaf\x9e\x19\x7f]\xad\x04\x119\x06\xa9\xcf\x9a*"),
        (0, 1, b"\x86\xf7\xe47\xfa\xa5\xa7\xfc\xe1]\x1d\xdc\xb9\xea\xea\xea7vg\xb8"),
    ),
)
async def test_file_bin_sha1(file_bin, offset, size, res):
    assert await file_bin.sha1(offset, size) == res


async def test_file_bin_read(file_bin):
    assert file_bin.offset == 0
    assert await file_bin.read() == b"abc"
    assert file_bin.offset == 3


async def test_file_bin_read_short(file_bin):
    assert file_bin.offset == 0
    assert await file_bin.read(2) == b"ab"
    assert file_bin.offset == 2
    assert await file_bin.read(2) == b"c"
    assert file_bin.offset == 3


async def test_file_bin_write(file_bin, tmp_path):
    file_bin.offset = 3
    await file_bin.write(b"def")
    assert file_bin.offset == 6
    with (tmp_path / "bin").open("rb") as file:
        assert file.read() == b"abcdef"


async def test_file_bin_flush(file_bin, tmp_path):
    # This does nothing so just call it that it won't fail
    await file_bin.flush()


async def test_file_bin_truncate(file_bin, tmp_path):
    await file_bin.truncate()
    with (tmp_path / "bin").open("rb") as file:
        assert file.read() == b""


@pytest.fixture(name="file_append")
def fixture_file_append(client, device, seed_tmp):
    return RpcFile(client, "test/device/files/bin", RpcFile.Flag.APPEND)


async def test_file_bin_append(file_append, tmp_path):
    file_append.offset = 0
    await file_append.write(b"def")
    file_append.offset = 0
    with (tmp_path / "bin").open("rb") as file:
        assert file.read() == b"abcdef"


@pytest.fixture(name="file_buffered")
def fixture_file_buffered(client, device, seed_tmp):
    return RpcFile(client, "test/device/files/bin", RpcFile.Flag.BUFFERED)


async def test_file_buffered_read(file_buffered, tmp_path):
    assert file_buffered.offset == 0
    assert await file_buffered.read(1) == b"a"
    assert file_buffered.offset == 1
    assert await file_buffered.read(1) == b"b"
    assert file_buffered.offset == 2
    assert await file_buffered.read(2) == b"c"
    assert file_buffered.offset == 3
    assert await file_buffered.read(2) == b""
    assert file_buffered.offset == 3
    with (tmp_path / "bin").open("ab") as file:
        file.write(b"def")
    assert await file_buffered.read(2) == b"de"
    assert file_buffered.offset == 5


async def test_file_buffered_write(file_buffered, tmp_path):
    file_buffered.offset = await file_buffered.size()
    await file_buffered.write(b"de")
    await file_buffered.write(b"f")
    with (tmp_path / "bin").open("rb") as file:
        assert file.read() == b"abc"
    await file_buffered.flush()
    with (tmp_path / "bin").open("rb") as file:
        assert file.read() == b"abcdef"

    await file_buffered.write(b"gh")
    file_buffered.offset = 1
    await file_buffered.write(b"d")
    with (tmp_path / "bin").open("rb") as file:
        assert file.read() == b"abcdefgh"
