"""Unit tests for persistence management functions.

These tests do NOT require a running container engine.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from pocketdock.errors import ContainerNotFound, PodmanNotRunning
from pocketdock.persistence import (
    _parse_container_list_item,
    _resolve_socket,
    destroy_container,
    list_containers,
    prune,
    resume_container,
    stop_container,
)
from pocketdock.types import ContainerListItem

# --- _resolve_socket ---


def test_resolve_socket_returns_provided() -> None:
    assert _resolve_socket("/tmp/custom.sock") == "/tmp/custom.sock"


def test_resolve_socket_detects_when_none() -> None:
    with patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/auto.sock"):
        assert _resolve_socket(None) == "/tmp/auto.sock"


def test_resolve_socket_raises_when_no_engine() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value=None),
        pytest.raises(PodmanNotRunning),
    ):
        _resolve_socket(None)


# --- _parse_container_list_item ---


def test_parse_full_data() -> None:
    data = {
        "Id": "abc123def45678",
        "Names": ["pd-test"],
        "State": "running",
        "Image": "pocketdock/minimal-python",
        "Labels": {
            "pocketdock.instance": "pd-test",
            "pocketdock.persist": "true",
            "pocketdock.created-at": "2026-02-09T00:00:00+00:00",
        },
    }
    item = _parse_container_list_item(data)
    assert item.id == "abc123def456"
    assert item.name == "pd-test"
    assert item.status == "running"
    assert item.image == "pocketdock/minimal-python"
    assert item.persist is True
    assert item.created_at == "2026-02-09T00:00:00+00:00"


def test_parse_no_labels_uses_names() -> None:
    data = {
        "Id": "abc123",
        "Names": ["/docker-name"],
        "State": "exited",
        "Image": "img",
        "Labels": {},
    }
    item = _parse_container_list_item(data)
    assert item.name == "docker-name"
    assert item.persist is False
    assert item.created_at == ""


def test_parse_empty_data() -> None:
    item = _parse_container_list_item({})
    assert item.id == ""
    assert item.name == ""
    assert item.status == "unknown"
    assert item.image == ""
    assert item.persist is False


def test_parse_none_labels() -> None:
    data = {"Id": "abc", "Labels": None, "Names": None, "State": "running", "Image": "img"}
    item = _parse_container_list_item(data)
    assert item.name == ""
    assert item.persist is False


def test_parse_persist_false_label() -> None:
    data = {
        "Id": "abc123def456",
        "Names": ["n"],
        "State": "running",
        "Image": "img",
        "Labels": {"pocketdock.persist": "false"},
    }
    item = _parse_container_list_item(data)
    assert item.persist is False


def test_container_list_item_is_frozen() -> None:
    item = ContainerListItem(
        id="x", name="n", status="running", image="img", created_at="", persist=False
    )
    with pytest.raises(AttributeError):
        item.name = "changed"  # type: ignore[misc]


# --- resume_container ---


async def test_resume_no_socket_raises() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value=None),
        pytest.raises(PodmanNotRunning),
    ):
        await resume_container("test")


async def test_resume_not_found() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[],
        ),
        pytest.raises(ContainerNotFound),
    ):
        await resume_container("missing")


async def test_resume_stopped_container_starts_it() -> None:
    list_result = [{"Id": "abc123", "State": "exited"}]
    inspect_result = {
        "Config": {
            "Image": "pocketdock/minimal-python",
            "Labels": {"pocketdock.persist": "true"},
        },
        "HostConfig": {"Memory": 0, "NanoCpus": 0},
    }
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=list_result,
        ),
        patch("pocketdock.persistence.sc.start_container", new_callable=AsyncMock) as start,
        patch(
            "pocketdock.persistence.sc.inspect_container",
            new_callable=AsyncMock,
            return_value=inspect_result,
        ),
    ):
        c = await resume_container("pd-test")

    start.assert_called_once_with("/tmp/s.sock", "abc123")
    assert c.container_id == "abc123"
    assert c.persist is True
    assert c.name == "pd-test"


async def test_resume_running_container_skips_start() -> None:
    list_result = [{"Id": "abc123", "State": "running"}]
    inspect_result = {
        "Config": {"Image": "img", "Labels": {}},
        "HostConfig": {"Memory": 268435456, "NanoCpus": 500000000},
    }
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=list_result,
        ),
        patch("pocketdock.persistence.sc.start_container", new_callable=AsyncMock) as start,
        patch(
            "pocketdock.persistence.sc.inspect_container",
            new_callable=AsyncMock,
            return_value=inspect_result,
        ),
    ):
        c = await resume_container("pd-test")

    start.assert_not_called()
    assert c.container_id == "abc123"


async def test_resume_with_explicit_socket() -> None:
    list_result = [{"Id": "abc123", "State": "exited"}]
    inspect_result = {
        "Config": {"Image": "img", "Labels": {}},
        "HostConfig": {"Memory": 0, "NanoCpus": 0},
    }
    with (
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=list_result,
        ) as list_mock,
        patch("pocketdock.persistence.sc.start_container", new_callable=AsyncMock),
        patch(
            "pocketdock.persistence.sc.inspect_container",
            new_callable=AsyncMock,
            return_value=inspect_result,
        ),
    ):
        await resume_container("pd-test", socket_path="/custom.sock")

    list_mock.assert_called_once_with("/custom.sock", label_filter="pocketdock.instance=pd-test")


# --- list_containers ---


async def test_list_no_socket_raises() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value=None),
        pytest.raises(PodmanNotRunning),
    ):
        await list_containers()


async def test_list_returns_parsed_items() -> None:
    raw = [
        {
            "Id": "abc123def456",
            "State": "running",
            "Image": "img",
            "Names": ["n"],
            "Labels": {"pocketdock.instance": "pd-a"},
        },
        {
            "Id": "def456abc123",
            "State": "exited",
            "Image": "img2",
            "Names": ["m"],
            "Labels": {"pocketdock.instance": "pd-b", "pocketdock.persist": "true"},
        },
    ]
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=raw,
        ),
    ):
        result = await list_containers()

    assert len(result) == 2
    assert all(isinstance(r, ContainerListItem) for r in result)
    assert result[0].name == "pd-a"
    assert result[1].persist is True


async def test_list_empty() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[],
        ),
    ):
        result = await list_containers()

    assert result == []


# --- destroy_container ---


async def test_destroy_no_socket_raises() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value=None),
        pytest.raises(PodmanNotRunning),
    ):
        await destroy_container("test")


async def test_destroy_not_found_raises() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[],
        ),
        pytest.raises(ContainerNotFound),
    ):
        await destroy_container("missing")


async def test_destroy_removes_with_force() -> None:
    inspect_data = {"Config": {"Labels": {}}}
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[{"Id": "abc123"}],
        ),
        patch(
            "pocketdock.persistence.sc.inspect_container",
            new_callable=AsyncMock,
            return_value=inspect_data,
        ),
        patch("pocketdock.persistence.sc.remove_container", new_callable=AsyncMock) as remove,
    ):
        await destroy_container("test")

    remove.assert_called_once_with("/tmp/s.sock", "abc123", force=True)


# --- prune ---


async def test_prune_no_socket_raises() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value=None),
        pytest.raises(PodmanNotRunning),
    ):
        await prune()


async def test_prune_removes_stopped_only() -> None:
    raw = [
        {"Id": "running1", "State": "running"},
        {"Id": "exited1", "State": "exited"},
        {"Id": "exited2", "State": "exited"},
    ]
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=raw,
        ),
        patch("pocketdock.persistence.sc.remove_container", new_callable=AsyncMock) as remove,
    ):
        count = await prune()

    assert count == 2
    assert remove.call_count == 2
    removed_ids = [call.args[1] for call in remove.call_args_list]
    assert "exited1" in removed_ids
    assert "exited2" in removed_ids
    assert "running1" not in removed_ids


async def test_prune_no_stopped_returns_zero() -> None:
    raw = [{"Id": "running1", "State": "running"}]
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=raw,
        ),
        patch("pocketdock.persistence.sc.remove_container", new_callable=AsyncMock) as remove,
    ):
        count = await prune()

    assert count == 0
    remove.assert_not_called()


async def test_prune_empty_list_returns_zero() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[],
        ),
    ):
        count = await prune()

    assert count == 0


# --- Sync wrappers (pocketdock.__init__) ---


def _close_coroutine_arg(mock_obj: MagicMock) -> None:
    """Close unawaited coroutine passed to mock to suppress warnings."""
    coro = mock_obj.run.call_args[0][0]
    coro.close()


def test_sync_resume_container() -> None:
    import pocketdock

    mock_lt = MagicMock()
    mock_lt.run.return_value = MagicMock()
    with patch.object(pocketdock, "_LoopThread") as lt_cls:
        lt_cls.get.return_value = mock_lt
        c = pocketdock.resume_container("pd-test", socket_path="/tmp/s.sock")

    lt_cls.get.assert_called_once()
    mock_lt.run.assert_called_once()
    _close_coroutine_arg(mock_lt)
    assert c is not None


def test_sync_list_containers() -> None:
    import pocketdock

    mock_lt = MagicMock()
    mock_lt.run.return_value = [
        ContainerListItem(
            id="abc", name="pd-a", status="running", image="img", created_at="", persist=False
        )
    ]
    with patch.object(pocketdock, "_LoopThread") as lt_cls:
        lt_cls.get.return_value = mock_lt
        result = pocketdock.list_containers(socket_path="/tmp/s.sock")

    lt_cls.get.assert_called_once()
    mock_lt.run.assert_called_once()
    _close_coroutine_arg(mock_lt)
    assert len(result) == 1
    assert result[0].name == "pd-a"


def test_sync_destroy_container() -> None:
    import pocketdock

    mock_lt = MagicMock()
    mock_lt.run.return_value = None
    with patch.object(pocketdock, "_LoopThread") as lt_cls:
        lt_cls.get.return_value = mock_lt
        pocketdock.destroy_container("pd-test", socket_path="/tmp/s.sock")

    lt_cls.get.assert_called_once()
    mock_lt.run.assert_called_once()
    _close_coroutine_arg(mock_lt)


def test_sync_prune() -> None:
    import pocketdock

    mock_lt = MagicMock()
    mock_lt.run.return_value = 3
    with patch.object(pocketdock, "_LoopThread") as lt_cls:
        lt_cls.get.return_value = mock_lt
        count = pocketdock.prune(socket_path="/tmp/s.sock")

    lt_cls.get.assert_called_once()
    mock_lt.run.assert_called_once()
    _close_coroutine_arg(mock_lt)
    assert count == 3


def test_sync_stop_container() -> None:
    import pocketdock

    mock_lt = MagicMock()
    mock_lt.run.return_value = None
    with patch.object(pocketdock, "_LoopThread") as lt_cls:
        lt_cls.get.return_value = mock_lt
        pocketdock.stop_container("myc", socket_path="/tmp/s.sock")

    lt_cls.get.assert_called_once()
    mock_lt.run.assert_called_once()
    _close_coroutine_arg(mock_lt)


# --- Project-scoped features (M7) ---


def test_parse_container_list_item_includes_project() -> None:
    data = {
        "Id": "abc123def456",
        "Names": ["pd-test"],
        "State": "running",
        "Image": "img",
        "Labels": {
            "pocketdock.instance": "pd-test",
            "pocketdock.project": "my-project",
        },
    }
    item = _parse_container_list_item(data)
    assert item.project == "my-project"


def test_parse_container_list_item_no_project() -> None:
    data = {
        "Id": "abc123",
        "Names": ["pd-test"],
        "State": "running",
        "Image": "img",
        "Labels": {},
    }
    item = _parse_container_list_item(data)
    assert item.project == ""


async def test_list_containers_with_project_filter() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[],
        ) as list_mock,
    ):
        await list_containers(project="my-project")

    list_mock.assert_called_once_with("/tmp/s.sock", label_filter="pocketdock.project=my-project")


async def test_prune_with_project_filter() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[],
        ) as list_mock,
    ):
        count = await prune(project="my-project")

    assert count == 0
    list_mock.assert_called_once_with("/tmp/s.sock", label_filter="pocketdock.project=my-project")


async def test_destroy_cleans_up_data_path(tmp_path: object) -> None:
    import pathlib

    data_dir = pathlib.Path(str(tmp_path)) / "instance-data"
    data_dir.mkdir()
    (data_dir / "test.txt").write_text("test")

    inspect_data = {
        "Config": {"Labels": {"pocketdock.data-path": str(data_dir)}},
    }
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[{"Id": "abc123"}],
        ),
        patch(
            "pocketdock.persistence.sc.inspect_container",
            new_callable=AsyncMock,
            return_value=inspect_data,
        ),
        patch("pocketdock.persistence.sc.remove_container", new_callable=AsyncMock),
    ):
        await destroy_container("test")

    assert not data_dir.exists()


async def test_destroy_no_data_path_label() -> None:
    inspect_data = {"Config": {"Labels": {}}}
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[{"Id": "abc123"}],
        ),
        patch(
            "pocketdock.persistence.sc.inspect_container",
            new_callable=AsyncMock,
            return_value=inspect_data,
        ),
        patch("pocketdock.persistence.sc.remove_container", new_callable=AsyncMock) as remove,
    ):
        await destroy_container("test")

    remove.assert_called_once()


async def test_destroy_data_path_already_gone() -> None:
    inspect_data = {
        "Config": {"Labels": {"pocketdock.data-path": "/nonexistent/path"}},
    }
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[{"Id": "abc123"}],
        ),
        patch(
            "pocketdock.persistence.sc.inspect_container",
            new_callable=AsyncMock,
            return_value=inspect_data,
        ),
        patch("pocketdock.persistence.sc.remove_container", new_callable=AsyncMock),
    ):
        # Should not raise even if data path doesn't exist
        await destroy_container("test")


async def test_resume_restores_project_and_data_path() -> None:
    list_result = [{"Id": "abc123", "State": "running"}]
    inspect_result = {
        "Config": {
            "Image": "img",
            "Labels": {
                "pocketdock.persist": "true",
                "pocketdock.project": "my-proj",
                "pocketdock.data-path": "/some/path",
            },
        },
        "HostConfig": {"Memory": 0, "NanoCpus": 0},
    }
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value="/tmp/s.sock"),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=list_result,
        ),
        patch("pocketdock.persistence.sc.start_container", new_callable=AsyncMock),
        patch(
            "pocketdock.persistence.sc.inspect_container",
            new_callable=AsyncMock,
            return_value=inspect_result,
        ),
    ):
        c = await resume_container("pd-test")

    assert c._project == "my-proj"
    assert c._data_path == "/some/path"


# --- stop_container ---


async def test_stop_container_success() -> None:
    container_data = [{"Id": "abc123", "State": "running"}]
    with (
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=container_data,
        ),
        patch(
            "pocketdock.persistence.sc.stop_container",
            new_callable=AsyncMock,
        ) as mock_stop,
    ):
        await stop_container("my-container", socket_path="/tmp/test.sock")

    mock_stop.assert_awaited_once_with("/tmp/test.sock", "abc123")


async def test_stop_container_not_found() -> None:
    with (
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=[],
        ),
        pytest.raises(ContainerNotFound),
    ):
        await stop_container("missing", socket_path="/tmp/test.sock")


async def test_stop_container_auto_detect_socket() -> None:
    container_data = [{"Id": "xyz789", "State": "running"}]
    with (
        patch(
            "pocketdock.persistence.sc.detect_socket",
            return_value="/tmp/auto.sock",
        ),
        patch(
            "pocketdock.persistence.sc.list_containers",
            new_callable=AsyncMock,
            return_value=container_data,
        ),
        patch(
            "pocketdock.persistence.sc.stop_container",
            new_callable=AsyncMock,
        ) as mock_stop,
    ):
        await stop_container("my-container")

    mock_stop.assert_awaited_once_with("/tmp/auto.sock", "xyz789")


async def test_stop_container_no_engine() -> None:
    with (
        patch("pocketdock.persistence.sc.detect_socket", return_value=None),
        pytest.raises(PodmanNotRunning),
    ):
        await stop_container("my-container")
