"""Comprehensive tests for AgenticCognition Python SDK."""
from __future__ import annotations

import json
import subprocess
from pathlib import Path, PurePosixPath
from unittest.mock import patch, MagicMock

import pytest

from agentic_cognition import CognitionStore, CognitionError, __version__


# ---------------------------------------------------------------------------
# 1. Package Metadata
# ---------------------------------------------------------------------------


class TestPackageMetadata:
    def test_version_exists(self) -> None:
        assert __version__ is not None
        assert isinstance(__version__, str)
        assert len(__version__) > 0

    def test_version_semver(self) -> None:
        parts = __version__.split(".")
        assert len(parts) == 3
        assert all(p.isdigit() for p in parts)

    def test_version_is_010(self) -> None:
        assert __version__ == "0.1.0"

    def test_import_main_class(self) -> None:
        assert CognitionStore is not None

    def test_import_error_class(self) -> None:
        assert CognitionError is not None
        assert issubclass(CognitionError, Exception)

    def test_main_class_has_docstring(self) -> None:
        assert CognitionStore.__doc__ is not None
        assert len(CognitionStore.__doc__) > 10


# ---------------------------------------------------------------------------
# 2. Initialization
# ---------------------------------------------------------------------------


class TestInit:
    def test_create_with_string_path(self, tmp_path: Path) -> None:
        path = str(tmp_path / "test.acog")
        obj = CognitionStore(path)
        assert str(obj.path) == path

    def test_create_with_path_object(self, tmp_path: Path) -> None:
        path = tmp_path / "test.acog"
        obj = CognitionStore(path)
        assert obj.path == path

    def test_create_with_pure_posix_path(self) -> None:
        obj = CognitionStore(PurePosixPath("/tmp/test.acog"))
        assert "test.acog" in str(obj.path)

    def test_path_converted_to_path_object(self, tmp_path: Path) -> None:
        path = str(tmp_path / "test.acog")
        obj = CognitionStore(path)
        assert isinstance(obj.path, Path)

    def test_custom_binary_name(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "test.acog"), binary="custom-bin")
        assert obj.binary == "custom-bin"

    def test_default_binary_name(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "test.acog"))
        assert obj.binary == "acog"

    def test_exists_false_for_new(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "nonexistent.acog"))
        assert not obj.exists

    def test_exists_true_when_file_present(self, tmp_path: Path) -> None:
        path = tmp_path / "exists.acog"
        path.touch()
        obj = CognitionStore(str(path))
        assert obj.exists

    def test_save_is_noop(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "test.acog"))
        obj.save()

    def test_repr_does_not_crash(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "test.acog"))
        r = repr(obj)
        assert isinstance(r, str)


# ---------------------------------------------------------------------------
# 3. Binary Resolution
# ---------------------------------------------------------------------------


class TestBinaryResolution:
    def test_missing_binary_raises(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"), binary="nonexistent-xyz-999")
        with pytest.raises(CognitionError):
            obj._find_binary()

    def test_error_contains_binary_name(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"), binary="nonexistent-xyz-999")
        with pytest.raises(CognitionError, match="nonexistent-xyz-999"):
            obj._find_binary()

    def test_error_contains_install_hint(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"), binary="nonexistent-xyz-999")
        with pytest.raises(CognitionError, match="Install"):
            obj._find_binary()

    def test_caches_result(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/fake/path/acog"
        assert obj._find_binary() == "/fake/path/acog"

    def test_cache_persists_across_calls(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/cached/bin"
        assert obj._find_binary() == "/cached/bin"
        assert obj._find_binary() == "/cached/bin"


# ---------------------------------------------------------------------------
# 4. Subprocess Execution
# ---------------------------------------------------------------------------


class TestSubprocessExecution:
    def test_run_calls_subprocess(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/usr/bin/echo"
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout="ok\n", stderr=""
            )
            result = obj._run("arg1", "arg2")
            assert mock_run.called
            cmd = mock_run.call_args[0][0]
            assert cmd[0] == "/usr/bin/echo"
            assert "arg1" in cmd
            assert "arg2" in cmd

    def test_run_includes_file_flag(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/usr/bin/echo"
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout="ok\n", stderr=""
            )
            obj._run("test")
            cmd = mock_run.call_args[0][0]
            assert "--file" in cmd
            assert str(tmp_path / "t.acog") in cmd

    def test_run_raises_on_nonzero_exit(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/bin/false"
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=1, stdout="", stderr="error happened"
            )
            with pytest.raises(CognitionError, match="error happened"):
                obj._run("fail")

    def test_run_returns_stripped_stdout(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/usr/bin/echo"
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout="  hello world  \n", stderr=""
            )
            result = obj._run("test")
            assert result == "hello world"

    def test_run_json_parses_output(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/usr/bin/echo"
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout='{"key": "value"}\n', stderr=""
            )
            result = obj._run_json("test")
            assert result == {"key": "value"}

    def test_run_json_raises_on_invalid_json(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/usr/bin/echo"
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout="not json at all", stderr=""
            )
            with pytest.raises((json.JSONDecodeError, CognitionError)):
                obj._run_json("test")

    def test_run_json_returns_empty_dict_on_empty_output(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/usr/bin/echo"
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout="", stderr=""
            )
            result = obj._run_json("test")
            assert result == {}


# ---------------------------------------------------------------------------
# 5. Edge Cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    def test_empty_path(self) -> None:
        obj = CognitionStore("")
        assert isinstance(obj.path, Path)

    def test_path_with_spaces(self, tmp_path: Path) -> None:
        path = tmp_path / "path with spaces" / "test.acog"
        obj = CognitionStore(str(path))
        assert "spaces" in str(obj.path)

    def test_path_with_unicode(self, tmp_path: Path) -> None:
        path = tmp_path / "donnees" / "test.acog"
        obj = CognitionStore(str(path))
        assert "donnees" in str(obj.path)

    def test_very_long_path(self, tmp_path: Path) -> None:
        long_name = "a" * 200
        path = tmp_path / long_name / "test.acog"
        obj = CognitionStore(str(path))
        assert len(str(obj.path)) > 200

    def test_save_idempotent(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj.save()
        obj.save()
        obj.save()

    def test_multiple_instances_independent(self, tmp_path: Path) -> None:
        a = CognitionStore(str(tmp_path / "a.acog"))
        b = CognitionStore(str(tmp_path / "b.acog"))
        assert a.path != b.path
        a._resolved_binary = "/path/a"
        assert b._resolved_binary is None

    def test_dot_in_directory_name(self, tmp_path: Path) -> None:
        path = tmp_path / "v1.0.0" / "test.acog"
        obj = CognitionStore(str(path))
        assert "v1.0.0" in str(obj.path)


# ---------------------------------------------------------------------------
# 6. Error Handling
# ---------------------------------------------------------------------------


class TestErrorHandling:
    def test_error_is_exception(self) -> None:
        assert issubclass(CognitionError, Exception)

    def test_error_stores_message(self) -> None:
        err = CognitionError("test message")
        assert "test message" in str(err)

    def test_error_caught_as_exception(self) -> None:
        with pytest.raises(Exception):
            raise CognitionError("boom")

    def test_error_caught_specifically(self) -> None:
        try:
            raise CognitionError("specific")
        except CognitionError as e:
            assert "specific" in str(e)

    def test_error_repr(self) -> None:
        err = CognitionError("repr test")
        assert repr(err) is not None


# ---------------------------------------------------------------------------
# 7. Stress Tests
# ---------------------------------------------------------------------------


class TestStress:
    def test_create_1000_instances(self, tmp_path: Path) -> None:
        instances = [
            CognitionStore(str(tmp_path / f"test_{i}.acog"))
            for i in range(1000)
        ]
        assert len(instances) == 1000
        assert instances[0].path != instances[999].path

    def test_find_binary_1000_cached(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        obj._resolved_binary = "/cached/bin"
        for _ in range(1000):
            assert obj._find_binary() == "/cached/bin"

    def test_save_100_times(self, tmp_path: Path) -> None:
        obj = CognitionStore(str(tmp_path / "t.acog"))
        for _ in range(100):
            obj.save()
