"""
tests/test_stepan_v45_farmos_assets.py

Unit tests for:
  - Patch 1: _safe_notes (whitespace normalization + word-boundary truncation)
  - Patch 2: _extract_name, _extract_label, _extract_date (extended JSON:API mapping)
  - Patch 3: _parse_jsonapi_list (data not list / not dict guards)
  - v4.5: _farmos_search_assets_impl (fail-closed asset search)
"""
from __future__ import annotations
import os
import sys
import types
import pytest
from unittest.mock import patch, MagicMock

# ── Paths ──────────────────────────────────────────────────────────────────────
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_PKG = os.path.join(_ROOT, "packages", "agromatrix-tools")
for _p in (_PKG,):
    if _p not in sys.path:
        sys.path.insert(0, _p)

# ── Mocks ──────────────────────────────────────────────────────────────────────
def _stub_module(name: str) -> types.ModuleType:
    m = types.ModuleType(name)
    sys.modules[name] = m
    return m

for _mod in ("nats", "nats.aio", "nats.aio.client", "crewai", "crewai.tools"):
    if _mod not in sys.modules:
        _stub_module(_mod)

sys.modules["crewai.tools"].tool = lambda name: (lambda f: f)  # type: ignore
sys.modules["nats.aio.client"].Client = MagicMock  # type: ignore

_audit_stub = types.ModuleType("agromatrix_tools.audit")
_audit_stub.audit_tool_call = lambda *a, **kw: None  # type: ignore
sys.modules["agromatrix_tools.audit"] = _audit_stub

import importlib as _il
if "agromatrix_tools.tool_farmos_read" in sys.modules:
    _il.reload(sys.modules["agromatrix_tools.tool_farmos_read"])

from agromatrix_tools.tool_farmos_read import (  # noqa: E402
    _safe_notes,
    _extract_name,
    _extract_label,
    _extract_date,
    _parse_jsonapi_list,
    _farmos_search_assets_impl,
    farmos_search_assets,
)


# ── Fixtures ───────────────────────────────────────────────────────────────────
@pytest.fixture(autouse=True)
def clear_farmos_env(monkeypatch):
    for k in ("FARMOS_BASE_URL", "FARMOS_TOKEN", "FARMOS_USER", "FARMOS_PASS", "FARMOS_PASSWORD"):
        monkeypatch.delenv(k, raising=False)
    yield


def _mock_resp(data, status: int = 200) -> MagicMock:
    resp = MagicMock()
    resp.status_code = status
    resp.json.return_value = data
    return resp


def _asset_item(label: str, asset_type: str = "asset--land", item_id: str = "abcdef12-3456-7890") -> dict:
    return {
        "type": asset_type,
        "id": item_id,
        "attributes": {"label": label},
    }


# ─── Patch 1: _safe_notes ─────────────────────────────────────────────────────

class TestSafeNotes:
    def test_short_unchanged(self):
        assert _safe_notes("hello world") == "hello world"

    def test_strips_leading_trailing(self):
        assert _safe_notes("  hello  ") == "hello"

    def test_normalizes_tabs(self):
        assert _safe_notes("a\tb") == "a b"

    def test_normalizes_newlines(self):
        assert _safe_notes("a\nb\nc") == "a b c"

    def test_normalizes_multiple_spaces(self):
        assert _safe_notes("a   b   c") == "a b c"

    def test_truncates_to_80(self):
        long = "word " * 30  # 150 chars
        result = _safe_notes(long)
        assert len(result) <= 80

    def test_truncates_on_word_boundary(self):
        # 80 chars that split a word
        s = "а " * 41  # "а а а а ..." — кожне слово 1 символ
        result = _safe_notes(s.strip(), max_len=10)
        # Не повинно закінчуватись посередині слова (пробіл)
        assert not result.endswith(" ")

    def test_no_utf8_split(self):
        # Рядок з кириличними символами — не повинен розрізати UTF-8 символ
        s = "абвгдежзиклмн " * 5  # ~70 символів
        result = _safe_notes(s, max_len=20)
        assert isinstance(result, str)
        assert len(result) <= 20

    def test_empty_string(self):
        assert _safe_notes("") == ""

    def test_exact_80_chars(self):
        s = "a" * 80
        assert _safe_notes(s) == s

    def test_81_chars_no_spaces_truncates_hard(self):
        s = "a" * 81
        result = _safe_notes(s, max_len=80)
        assert len(result) <= 80


# ─── Patch 2: _extract_name / _extract_label / _extract_date ─────────────────

class TestExtractName:
    def test_name_field(self):
        assert _extract_name({"name": "Поле 1"}) == "Поле 1"

    def test_label_fallback(self):
        assert _extract_name({"label": "Поле 2"}) == "Поле 2"

    def test_type_fallback(self):
        assert _extract_name({"type": "activity"}) == "activity"

    def test_explicit_fallback(self):
        assert _extract_name({}, fallback="def") == "def"

    def test_normalizes_whitespace(self):
        assert _extract_name({"name": "  Поле\t1  "}) == "Поле 1"

    def test_prefers_name_over_label(self):
        assert _extract_name({"name": "A", "label": "B"}) == "A"


class TestExtractLabel:
    def test_label_field(self):
        item = {"attributes": {"label": "Поле A"}}
        assert _extract_label(item) == "Поле A"

    def test_name_fallback(self):
        item = {"attributes": {"name": "Поле B"}}
        assert _extract_label(item) == "Поле B"

    def test_type_fallback(self):
        item = {"attributes": {"type": "land"}}
        assert _extract_label(item) == "land"

    def test_no_attrs(self):
        item = {}
        assert _extract_label(item) == "(no label)"

    def test_non_dict(self):
        assert _extract_label("string") == "(no label)"  # type: ignore[arg-type]


class TestExtractDate:
    def test_timestamp_int(self):
        result = _extract_date({"timestamp": 1700000000})
        assert result.startswith("2023-")

    def test_changed_fallback(self):
        result = _extract_date({"changed": 1700000000})
        assert result.startswith("2023-")

    def test_created_fallback(self):
        result = _extract_date({"created": 1700000000})
        assert result.startswith("2023-")

    def test_string_date(self):
        result = _extract_date({"timestamp": "2023-11-15T10:00:00Z"})
        assert result == "2023-11-15"

    def test_no_date_fields(self):
        assert _extract_date({}) == "—"

    def test_prefers_timestamp_over_changed(self):
        r = _extract_date({"timestamp": 1700000000, "changed": 1600000000})
        assert r.startswith("2023-")


# ─── Patch 3: _parse_jsonapi_list ─────────────────────────────────────────────

class TestParseJsonapiList:
    def test_valid_list(self):
        resp = _mock_resp({"data": [{"id": "1"}]})
        items, err = _parse_jsonapi_list(resp)
        assert err is None
        assert len(items) == 1

    def test_empty_list(self):
        resp = _mock_resp({"data": []})
        items, err = _parse_jsonapi_list(resp)
        assert err is None
        assert items == []

    def test_data_none(self):
        resp = _mock_resp({"data": None})
        items, err = _parse_jsonapi_list(resp)
        assert err is None
        assert items == []

    def test_data_missing(self):
        resp = _mock_resp({"other": "value"})
        items, err = _parse_jsonapi_list(resp)
        assert err is None
        assert items == []

    def test_data_not_list(self):
        """Patch 3: data is a dict, not list → error string."""
        resp = _mock_resp({"data": {"id": "1"}})
        items, err = _parse_jsonapi_list(resp)
        assert err is not None
        assert "не є списком" in err or "неочікуваний" in err

    def test_response_not_dict(self):
        """Patch 3: entire response is a list, not dict."""
        resp = _mock_resp([{"id": "1"}])
        items, err = _parse_jsonapi_list(resp)
        assert err is not None
        assert "неочікуваний" in err

    def test_invalid_json(self):
        resp = MagicMock()
        resp.json.side_effect = ValueError("no json")
        items, err = _parse_jsonapi_list(resp)
        assert err is not None
        assert "json" in err.lower() or "розібрати" in err.lower()


# ─── v4.5: _farmos_search_assets_impl ────────────────────────────────────────

class TestFarmosSearchAssetsImpl:
    # ── Config guards ──────────────────────────────────────────────────────────
    def test_no_base_url(self):
        result = _farmos_search_assets_impl()
        assert "FARMOS_BASE_URL" in result

    def test_base_url_no_auth(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        result = _farmos_search_assets_impl()
        assert "токен" in result.lower() or "логін" in result.lower()

    def test_invalid_asset_type(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        result = _farmos_search_assets_impl(asset_type="badtype")
        assert "asset_type має бути" in result
        assert "asset_land" in result

    def test_limit_clamped(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = _mock_resp({"data": []})
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl(asset_type="asset_land", limit=100)
        assert isinstance(result, str)

    def test_name_contains_truncated(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = _mock_resp({"data": []})
        long_name = "a" * 200
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl(asset_type="asset_land", name_contains=long_name)
        # Не падає, повертає рядок
        assert isinstance(result, str)

    # ── HTTP responses ─────────────────────────────────────────────────────────
    def test_200_empty(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = _mock_resp({"data": []})
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl()
        assert "нічого не знайдено" in result

    def test_200_with_items(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [
            _asset_item("Поле Північ", "asset--land", "aaa-111"),
            _asset_item("Поле Південь", "asset--land", "bbb-222"),
        ]
        mock_resp = _mock_resp({"data": items})
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl()
        assert "Поле Північ" in result
        assert "Поле Південь" in result

    def test_output_format(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [_asset_item("Тест", "asset--land", "12345678-abcd")]
        mock_resp = _mock_resp({"data": items})
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl()
        lines = [l for l in result.split("\n") if l.strip()]
        assert lines[0].startswith("- ")
        assert lines[0].count("|") >= 2
        # id= лише перші 8 символів
        assert "id=12345678" in lines[0]
        assert "abcd" not in lines[0]

    def test_client_side_filter(self, monkeypatch):
        """name_contains фільтрує client-side якщо server не підтримує filter."""
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [
            _asset_item("Поле А"),
            _asset_item("Сад Б"),
        ]
        # Перша спроба з filter → повертає всі (сервер ігнорує filter),
        # але client-side відфільтровує
        mock_resp = _mock_resp({"data": items})
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl(asset_type="asset_land", name_contains="поле")
        assert "Поле А" in result
        assert "Сад Б" not in result

    def test_client_side_filter_case_insensitive(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [_asset_item("ПОЛЕ ЗАХІД")]
        mock_resp = _mock_resp({"data": items})
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl(asset_type="asset_land", name_contains="поле")
        assert "ПОЛЕ ЗАХІД" in result

    def test_output_truncated_to_12(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [_asset_item(f"Поле {i}") for i in range(20)]
        mock_resp = _mock_resp({"data": items})
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl()
        lines = [l for l in result.split("\n") if l.startswith("- ")]
        assert len(lines) == 12

    def test_line_max_120_chars(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        long_label = "Л" * 200
        items = [_asset_item(long_label)]
        mock_resp = _mock_resp({"data": items})
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl()
        for line in result.split("\n"):
            assert len(line) <= 120

    def test_404(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = MagicMock()
        mock_resp.status_code = 404
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl()
        assert "404" in result or "не знайдено" in result

    def test_401(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "badtok")
        mock_resp = MagicMock()
        mock_resp.status_code = 401
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl()
        assert "401" in result or "авторизац" in result.lower()

    def test_timeout(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        import requests as _r
        with patch("agromatrix_tools.tool_farmos_read.requests.get",
                   side_effect=_r.exceptions.Timeout()):
            result = _farmos_search_assets_impl()
        assert "timeout" in result.lower()

    def test_connect_error(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        import requests as _r
        with patch("agromatrix_tools.tool_farmos_read.requests.get",
                   side_effect=_r.exceptions.ConnectionError("refused")):
            result = _farmos_search_assets_impl()
        assert "мережна помилка" in result or "недоступний" in result.lower()

    def test_never_reveals_url(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://secret.internal")
        monkeypatch.setenv("FARMOS_TOKEN", "secrettoken")
        mock_resp = _mock_resp({"data": [_asset_item("Поле")]})
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl()
        assert "secret.internal" not in result
        assert "secrettoken" not in result

    def test_never_raises(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        with patch("agromatrix_tools.tool_farmos_read.requests.get",
                   side_effect=RuntimeError("boom")):
            result = _farmos_search_assets_impl()
        assert isinstance(result, str)
        assert len(result) > 0

    def test_data_not_list_handled(self, monkeypatch):
        """Patch 3: якщо data — dict, а не list → зрозумілий вивід."""
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = _mock_resp({"data": {"id": "1"}})  # не list!
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_search_assets_impl()
        assert "неочікуваний" in result or "формат" in result

    # ── Tool object ────────────────────────────────────────────────────────────
    def test_tool_object_name(self):
        assert hasattr(farmos_search_assets, "name")
        assert farmos_search_assets.name == "farmos_search_assets"

    def test_all_asset_types_accepted(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = _mock_resp({"data": []})
        for at in ("asset_land", "asset_plant", "asset_equipment",
                    "asset_structure", "asset_animal"):
            with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
                result = _farmos_search_assets_impl(asset_type=at)
            assert "asset_type має бути" not in result, f"whitelist rejected: {at}"
