"""
tests/test_stepan_v44_farmos_logs.py

Unit tests for v4.4 FarmOS read-only logs tool + connection error normalization.

Scope:
  - agromatrix_tools.tool_farmos_read._farmos_read_logs_impl (fail-closed)
  - agromatrix_tools.tool_farmos_read._classify_exception (error normalization)
  - crews.agromatrix_crew.operator_commands.handle_farmos_status (logs subcommand)
"""
from __future__ import annotations
import json
import os
import sys
import types
import pytest
from unittest.mock import patch, MagicMock

# ── Шляхи ─────────────────────────────────────────────────────────────────────
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_PKG = os.path.join(_ROOT, "packages", "agromatrix-tools")
_CREWS = os.path.join(_ROOT, "crews")
for _p in (_PKG, _CREWS):
    if _p not in sys.path:
        sys.path.insert(0, _p)

# ── Мок зовнішніх модулів ─────────────────────────────────────────────────────
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
    _farmos_read_logs_impl,
    _classify_exception,
)
from crews.agromatrix_crew.operator_commands import handle_farmos_status  # noqa: E402


# ── 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 _make_json_resp(items: list, status: int = 200) -> MagicMock:
    resp = MagicMock()
    resp.status_code = status
    resp.json.return_value = {"data": items}
    return resp


def _make_log_item(name: str = "Полив", ts: int = 1700000000, notes: str = "без змін") -> dict:
    return {
        "type": "log--activity",
        "id": "abc-123",
        "attributes": {
            "name": name,
            "timestamp": ts,
            "notes": {"value": notes},
        },
    }


# ─── _classify_exception ──────────────────────────────────────────────────────

class TestClassifyException:
    def test_timeout(self):
        import requests
        assert _classify_exception(requests.exceptions.Timeout()) == "timeout"

    def test_connect(self):
        import requests
        assert _classify_exception(requests.exceptions.ConnectionError("refused")) == "connect"

    def test_dns(self):
        exc = Exception("Name or service not known")
        assert _classify_exception(exc) == "dns"

    def test_ssl(self):
        exc = Exception("SSL: CERTIFICATE_VERIFY_FAILED")
        assert _classify_exception(exc) == "ssl"

    def test_other(self):
        exc = ValueError("something")
        assert _classify_exception(exc) == "other"


# ─── _farmos_read_logs_impl ───────────────────────────────────────────────────

class TestFarmosReadLogsImpl:
    # ── Config guards ──────────────────────────────────────────────────────────
    def test_no_base_url(self):
        result = _farmos_read_logs_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_read_logs_impl()
        assert "токен" in result.lower() or "логін" in result.lower()

    def test_invalid_log_type(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        result = _farmos_read_logs_impl(log_type="badtype")
        assert "log_type має бути" in result
        # Whitelist перераховано у відповіді
        for t in ("activity", "observation"):
            assert t in result

    def test_limit_clamped_min(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = _make_json_resp([], status=200)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_impl(limit=-5)
        # Не падає, повертає "записів не знайдено"
        assert isinstance(result, str)

    def test_limit_clamped_max(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [_make_log_item(f"item {i}") for i in range(30)]
        mock_resp = _make_json_resp(items, status=200)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_impl(limit=100)
        # Обмежено до _LOG_OUTPUT_MAX_LINES=12
        lines = [l for l in result.split("\n") if l.strip().startswith("-")]
        assert len(lines) <= 12

    # ── HTTP responses ─────────────────────────────────────────────────────────
    def test_200_empty(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = _make_json_resp([], status=200)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_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 = [
            _make_log_item("Внесення добрив", 1700010000, "карбамід 200кг/га"),
            _make_log_item("Оранка", 1700020000, "глибина 25см"),
        ]
        mock_resp = _make_json_resp(items, status=200)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_impl()
        assert "Внесення добрив" in result
        assert "Оранка" in result
        assert "2023-" in result  # перевіряємо що дата парситься

    def test_200_output_format(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [_make_log_item("Тест", 1700000000, "примітка")]
        mock_resp = _make_json_resp(items, status=200)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_impl()
        # Кожен рядок має формат "- name | date | notes"
        lines = [l for l in result.split("\n") if l.strip()]
        assert len(lines) >= 1
        assert lines[0].startswith("- ")
        assert lines[0].count("|") >= 2

    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_read_logs_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_read_logs_impl()
        assert "401" in result or "авторизац" in result.lower()

    def test_500(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = MagicMock()
        mock_resp.status_code = 500
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_impl()
        assert "500" 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_read_logs_impl()
        assert "timeout" in result.lower()

    def test_connection_error_dns(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        exc = Exception("Name or service not known")
        with patch("agromatrix_tools.tool_farmos_read.requests.get", side_effect=exc):
            result = _farmos_read_logs_impl()
        assert isinstance(result, str)
        assert "внутрішня помилка" in result.lower()

    def test_invalid_json(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.side_effect = ValueError("not json")
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_impl()
        assert "json" in result.lower() or "розібрати" in result.lower()

    def test_never_reveals_url(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://secret-farmos.internal")
        monkeypatch.setenv("FARMOS_TOKEN", "secrettoken")
        mock_resp = _make_json_resp([_make_log_item()], status=200)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_impl()
        assert "secret-farmos" 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_read_logs_impl()
        assert isinstance(result, str)
        assert len(result) > 0

    def test_output_truncated_to_12(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [_make_log_item(f"item {i}") for i in range(20)]
        mock_resp = _make_json_resp(items, status=200)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_impl()
        lines = [l for l in result.split("\n") if l.startswith("- ")]
        assert len(lines) == 12

    def test_notes_truncated_to_80(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        long_notes = "A" * 200
        items = [_make_log_item("Test", 1700000000, long_notes)]
        mock_resp = _make_json_resp(items, status=200)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = _farmos_read_logs_impl()
        line = [l for l in result.split("\n") if l.startswith("- ")][0]
        notes_part = line.split("|")[-1].strip()
        assert len(notes_part) <= 80


# ─── /farmos logs operator command ────────────────────────────────────────────

class TestFarmosLogsCommand:
    def test_logs_no_env(self):
        result = handle_farmos_status(["logs"])
        assert isinstance(result, dict)
        assert "FARMOS_BASE_URL" in result["summary"] or "FarmOS" in result["summary"]

    def test_logs_with_log_type(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [_make_log_item("Сівба", 1700000000, "пшениця")]
        mock_resp = _make_json_resp(items)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = handle_farmos_status(["logs", "seeding"])
        assert "Сівба" in result["summary"]

    def test_logs_with_invalid_log_type(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        result = handle_farmos_status(["logs", "badtype"])
        assert "log_type" in result["summary"] or "має бути" in result["summary"]

    def test_logs_with_limit(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        items = [_make_log_item(f"item {i}") for i in range(5)]
        mock_resp = _make_json_resp(items)
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = handle_farmos_status(["logs", "activity", "5"])
        lines = [l for l in result["summary"].split("\n") if l.startswith("- ")]
        assert len(lines) == 5

    def test_logs_invalid_limit_defaults(self, monkeypatch):
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = _make_json_resp([_make_log_item()])
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = handle_farmos_status(["logs", "activity", "notanumber"])
        assert isinstance(result, dict)

    def test_unknown_subcommand_updated_message(self):
        result = handle_farmos_status(["unknown_sub"])
        assert "підтримується" in result["summary"].lower()
        assert "logs" in result["summary"].lower()

    def test_internal_error_fallback_logs(self):
        with patch("agromatrix_tools.tool_farmos_read._farmos_read_logs_impl",
                   side_effect=RuntimeError("crash")):
            result = handle_farmos_status(["logs"])
        assert "помилка" in result["summary"].lower() or "FarmOS" in result["summary"]

    def test_status_still_works(self, monkeypatch):
        """Переконуємось що farmos status не зломано v4.4."""
        monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
        monkeypatch.setenv("FARMOS_TOKEN", "tok")
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
            result = handle_farmos_status([])
        assert "доступний" in result["summary"].lower()
