import re
from datetime import datetime
from struct import pack
from time import sleep
from typing import TYPE_CHECKING, Any, Dict, List

import msgpack
import pytest
from sqlalchemy import select

from dql.cli import ls

from ..utils import uppercase_scheme

if TYPE_CHECKING:
    from dql.data_storage import AbstractMetastore


def same_lines(lines1, lines2):
    return sorted(lines1.split("\n")) == sorted(lines2.split("\n"))


def test_ls_no_args(cloud_test_catalog, capsys):
    src = cloud_test_catalog.src_uri
    catalog = cloud_test_catalog.catalog
    catalog.index([src])
    ls([], catalog=catalog)
    captured = capsys.readouterr()
    assert captured.out == f"{src}\n"


def test_ls_root(cloud_test_catalog, cloud_type, capsys):
    src = cloud_test_catalog.src_uri
    root = src[: src.find("://") + 3]
    src_name = src.replace(root, "", 1)
    ls([root], catalog=cloud_test_catalog.catalog)
    captured = capsys.readouterr()
    if cloud_type == "file":
        assert not captured.out
    else:
        buckets = captured.out.split("\n")
        assert src_name in buckets


ls_sources_output = """\
cats/
dogs/
description

dogs/others:
dog4

dogs:
dog1
dog2
dog3
"""


def test_ls_sources(cloud_test_catalog, capsys):
    src = cloud_test_catalog.src_uri
    ls([src, f"{src}/dogs/*"], catalog=cloud_test_catalog.catalog)
    captured = capsys.readouterr()
    assert same_lines(captured.out, ls_sources_output)


def test_ls_sources_scheme_uppercased(cloud_test_catalog, capsys):
    src = uppercase_scheme(cloud_test_catalog.src_uri)
    ls([src, f"{src}/dogs/*"], catalog=cloud_test_catalog.catalog)
    captured = capsys.readouterr()
    assert same_lines(captured.out, ls_sources_output)


def test_ls_not_found(cloud_test_catalog):
    src = cloud_test_catalog.src_uri
    with pytest.raises(FileNotFoundError):
        ls([src, f"{src}/cats/bogus*"], catalog=cloud_test_catalog.catalog)


def test_ls_not_a_directory(cloud_test_catalog):
    src = cloud_test_catalog.src_uri
    with pytest.raises(FileNotFoundError):
        ls([src, f"{src}/description/"], catalog=cloud_test_catalog.catalog)


ls_glob_output = """\
dogs/others:
dog4

dogs:
dog1
dog2
dog3
"""


def test_ls_glob_sub(cloud_test_catalog, capsys):
    src = cloud_test_catalog.src_uri
    ls([f"{src}/dogs/*"], catalog=cloud_test_catalog.catalog)
    captured = capsys.readouterr()
    assert same_lines(captured.out, ls_glob_output)


def get_partial_indexed_paths(metastore: "AbstractMetastore"):
    p = metastore.partials
    return [
        r[0] for r in metastore.db.execute(select(p.c.path_str).order_by(p.c.path_str))
    ]


def test_ls_partial_indexing(cloud_test_catalog, capsys):
    metastore = cloud_test_catalog.catalog.metastore
    src = cloud_test_catalog.src_uri
    src_metastore = metastore.clone(src)

    ls([f"{src}/dogs/others/"], catalog=cloud_test_catalog.catalog)
    # These sleep calls are here to ensure that capsys can fully capture the output
    # and to avoid any flaky tests due to multithreading generating output out of order
    sleep(0.05)
    captured = capsys.readouterr()
    assert get_partial_indexed_paths(src_metastore) == ["dogs/others/"]
    assert "Listing" in captured.err
    assert "dog4\n" == captured.out

    ls([f"{src}/cats/"], catalog=cloud_test_catalog.catalog)
    sleep(0.05)
    captured = capsys.readouterr()
    assert get_partial_indexed_paths(src_metastore) == ["cats/", "dogs/others/"]
    assert "Listing" in captured.err
    assert same_lines("cat1\ncat2\n", captured.out)

    ls([f"{src}/dogs/"], catalog=cloud_test_catalog.catalog)
    sleep(0.05)
    captured = capsys.readouterr()
    assert get_partial_indexed_paths(src_metastore) == [
        "cats/",
        "dogs/",
        "dogs/others/",
    ]
    assert "Listing" in captured.err
    assert same_lines("others/\ndog1\ndog2\ndog3\n", captured.out)

    ls([f"{src}/cats/"], catalog=cloud_test_catalog.catalog)
    sleep(0.05)
    captured = capsys.readouterr()
    assert get_partial_indexed_paths(src_metastore) == [
        "cats/",
        "dogs/",
        "dogs/others/",
    ]
    assert "Listing" not in captured.err
    assert same_lines("cat1\ncat2\n", captured.out)

    ls([f"{src}/"], catalog=cloud_test_catalog.catalog)
    sleep(0.05)
    captured = capsys.readouterr()
    assert get_partial_indexed_paths(src_metastore) == [
        "",
        "cats/",
        "dogs/",
        "dogs/others/",
    ]
    assert "Listing" in captured.err
    assert same_lines("cats/\ndogs/\ndescription\n", captured.out)


class MockResponse:
    def __init__(self, content, ok=True):
        self.content = content
        self.ok = ok


def mock_post(url, data=None, json=None, **kwargs):
    source = json["source"]
    path = re.sub(r"\w+://[^/]+/?", "", source).rstrip("/")
    data = [
        {
            **d,
            "path_str": d["path_str"].format(src=path),
            "path": d["path_str"].format(src=path).split("/"),
        }
        for d in REMOTE_DATA[path]
    ]
    return MockResponse(
        content=msgpack.packb({"data": data}, default=_pack_extended_types)
    )


def _pack_extended_types(obj):
    if isinstance(obj, datetime):
        if obj.tzinfo:
            data = (obj.timestamp(), int(obj.utcoffset().total_seconds()))
            return msgpack.ExtType(42, pack("!dl", *data))
        else:
            data = (obj.timestamp(),)
            return msgpack.ExtType(42, pack("!d", *data))
    raise TypeError(f"Unknown type: {type(obj)}")


ls_remote_sources_output = """\
{src}:
cats/
dogs/
description

{src}/dogs/others:
dog4

{src}/dogs:
dog1
dog2
dog3
"""


def test_ls_remote_sources(cloud_type, capsys, monkeypatch):
    src = f"{cloud_type}://bucket"
    token = "35NmrvSlsGVxTYIglxSsBIQHRrMpi6irSSYcAL0flijOytCHc"  # noqa: S105
    with monkeypatch.context() as m:
        m.setattr("requests.post", mock_post)
        ls(
            [src, f"{src}/dogs/others", f"{src}/dogs"],
            config={
                "type": "http",
                "url": "http://localhost:8111/api/dql",
                "username": "dql-team",
                "token": f"isat_{token}",
            },
        )
    captured = capsys.readouterr()
    assert captured.out == ls_remote_sources_output.format(src=src)


owner_id = "a13a3ff923430363b098ce9c769e450724e74e646332b08ca6b3ac4f96dae083"
REMOTE_DATA: Dict[str, List[Dict[str, Any]]] = {
    "": [
        {
            "id": 816,
            "dir_type": 1,
            "parent_id": None,
            "name": "cats",
            "checksum": "",
            "etag": "",
            "version": "",
            "is_latest": True,
            "last_modified": datetime(2023, 1, 17, 21, 39, 0, 88564),
            "size": 0,
            "owner_name": "",
            "owner_id": "",
            "path_str": "{src}/cats",
            "anno": None,
            "path": [],
        },
        {
            "id": 825,
            "dir_type": 1,
            "parent_id": None,
            "name": "dogs",
            "checksum": "",
            "etag": "",
            "version": "",
            "is_latest": True,
            "last_modified": datetime(2023, 1, 17, 21, 39, 0, 88567),
            "size": 0,
            "owner_name": "",
            "owner_id": "",
            "path_str": "{src}/dogs",
            "anno": None,
            "path": [],
        },
        {
            "id": None,
            "dir_type": 0,
            "parent_id": None,
            "name": "description",
            "checksum": "",
            "etag": "20664550afa2654017377ceb266a1f82",
            "version": "",
            "is_latest": True,
            "last_modified": datetime(2022, 2, 10, 3, 39, 9),
            "size": 350496,
            "owner_name": "",
            "owner_id": owner_id,
            "path_str": "{src}/description",
            "anno": None,
            "path": [],
        },
    ],
    "dogs/others": [
        {
            "id": None,
            "dir_type": 0,
            "parent_id": None,
            "name": "dog4",
            "checksum": "",
            "etag": "c4e42ce24d92bb5b4c4be9a99b237502",
            "version": "",
            "is_latest": True,
            "last_modified": datetime(2022, 6, 28, 22, 39, 1),
            "size": 32975,
            "owner_name": "",
            "owner_id": owner_id,
            "path_str": "{src}/dogs/others/dog4",
            "anno": None,
            "path": [],
        },
    ],
    "dogs": [
        {
            "id": None,
            "dir_type": 0,
            "parent_id": None,
            "name": "dog1",
            "checksum": "",
            "etag": "44a632238558e0aa4c54bdb901bf9cff",
            "version": "",
            "is_latest": True,
            "last_modified": datetime(2022, 6, 28, 22, 39, 1),
            "size": 101,
            "owner_name": "",
            "owner_id": owner_id,
            "path_str": "{src}/dogs/dog1",
            "anno": None,
            "path": [],
        },
        {
            "id": None,
            "dir_type": 0,
            "parent_id": None,
            "name": "dog2",
            "checksum": "",
            "etag": "76556c960239c50e5a8f8569daf85355",
            "version": "",
            "is_latest": True,
            "last_modified": datetime(2022, 6, 28, 22, 39, 1),
            "size": 29759,
            "owner_name": "",
            "owner_id": owner_id,
            "path_str": "{src}/dogs/dog2",
            "anno": None,
            "path": [],
        },
        {
            "id": None,
            "dir_type": 0,
            "parent_id": None,
            "name": "dog3",
            "checksum": "",
            "etag": "b1c99fedcf77bf5fa62984e93db1955c",
            "version": "",
            "is_latest": True,
            "last_modified": datetime(2022, 6, 28, 22, 39, 1),
            "size": 102,
            "owner_name": "",
            "owner_id": owner_id,
            "path_str": "{src}/dogs/dog3",
            "anno": None,
            "path": [],
        },
    ],
}
