"""
Tests for version compatibility warnings (P2-5) and framework attribute schema (P2-6).

Covers:
    P2-5: _parse_version, check_version_compatibility, COMPATIBLE_VERSIONS
    P2-6: FRAMEWORK_ATTR_SCHEMA, validate_framework_attributes, create_framework_attributes
"""

from __future__ import annotations

import logging
from unittest.mock import patch

import pytest

from risicare.integrations._base import (
    COMPATIBLE_VERSIONS,
    FRAMEWORK_ATTR_SCHEMA,
    _REQUIRED_ATTRS,
    _RECOMMENDED_ATTRS,
    _OPTIONAL_ATTRS,
    _parse_version,
    _warned_keys,
    check_version_compatibility,
    create_framework_attributes,
    validate_framework_attributes,
)


# =============================================================================
# P2-5: Version Parsing
# =============================================================================


class TestParseVersion:
    """Tests for the _parse_version helper."""

    def test_simple_version(self):
        assert _parse_version("1.2.3") == (1, 2, 3)

    def test_two_part_version(self):
        assert _parse_version("0.2") == (0, 2)

    def test_single_part_version(self):
        assert _parse_version("3") == (3,)

    def test_rc_suffix_stripped(self):
        assert _parse_version("1.0.0rc1") == (1, 0, 0)

    def test_dev_suffix_stripped(self):
        assert _parse_version("2.3.1dev4") == (2, 3, 1)

    def test_post_suffix_stripped(self):
        assert _parse_version("1.0.0.post1") == (1, 0, 0)

    def test_alpha_suffix_stripped(self):
        assert _parse_version("0.5.0a1") == (0, 5, 0)

    def test_beta_suffix_stripped(self):
        assert _parse_version("0.5.0b2") == (0, 5, 0)

    def test_empty_string_returns_zero(self):
        assert _parse_version("") == (0,)

    def test_comparison_works(self):
        assert _parse_version("0.2.0") < _parse_version("0.3.0")
        assert _parse_version("1.0.0") > _parse_version("0.99.0")
        assert _parse_version("0.2.1") == _parse_version("0.2.1")


# =============================================================================
# P2-5: Version Compatibility Checks
# =============================================================================


class TestCheckVersionCompatibility:
    """Tests for check_version_compatibility."""

    def setup_method(self):
        """Clear _warn_once cache before each test."""
        _warned_keys.clear()

    def test_compatible_version_returns_true(self):
        with patch(
            "risicare.integrations._base.get_framework_version",
            return_value="0.3.0",
        ):
            assert check_version_compatibility("langchain") is True

    def test_old_version_returns_false(self):
        with patch(
            "risicare.integrations._base.get_framework_version",
            return_value="0.1.0",
        ):
            assert check_version_compatibility("langchain") is False

    def test_too_new_version_returns_false(self):
        with patch(
            "risicare.integrations._base.get_framework_version",
            return_value="1.0.0",
        ):
            assert check_version_compatibility("langchain") is False

    def test_unknown_framework_returns_true(self):
        assert check_version_compatibility("nonexistent-framework") is True

    def test_undetectable_version_returns_true(self):
        with patch(
            "risicare.integrations._base.get_framework_version",
            return_value=None,
        ):
            assert check_version_compatibility("langchain") is True

    def test_incompatible_logs_warning(self, caplog):
        with patch(
            "risicare.integrations._base.get_framework_version",
            return_value="0.0.1",
        ):
            with caplog.at_level(logging.WARNING):
                check_version_compatibility("crewai")
            assert "outside the tested range" in caplog.text

    def test_compatible_does_not_log_warning(self, caplog):
        with patch(
            "risicare.integrations._base.get_framework_version",
            return_value="0.30.0",
        ):
            with caplog.at_level(logging.WARNING):
                check_version_compatibility("crewai")
            assert "outside the tested range" not in caplog.text

    def test_all_frameworks_have_valid_ranges(self):
        """Every entry in COMPATIBLE_VERSIONS must have min <= max."""
        for framework, (min_str, max_str) in COMPATIBLE_VERSIONS.items():
            min_v = _parse_version(min_str)
            max_v = _parse_version(max_str)
            assert min_v <= max_v, (
                f"{framework}: min {min_str} > max {max_str}"
            )


# =============================================================================
# P2-6: Framework Attribute Schema
# =============================================================================


class TestFrameworkAttrSchema:
    """Tests for the framework attribute schema."""

    def test_schema_has_all_sections(self):
        assert "required" in FRAMEWORK_ATTR_SCHEMA
        assert "recommended" in FRAMEWORK_ATTR_SCHEMA
        assert "optional" in FRAMEWORK_ATTR_SCHEMA

    def test_required_attrs_include_name_and_version(self):
        assert "framework.name" in _REQUIRED_ATTRS
        assert "framework.version" in _REQUIRED_ATTRS

    def test_no_overlap_between_categories(self):
        assert _REQUIRED_ATTRS.isdisjoint(_RECOMMENDED_ATTRS)
        assert _REQUIRED_ATTRS.isdisjoint(_OPTIONAL_ATTRS)
        assert _RECOMMENDED_ATTRS.isdisjoint(_OPTIONAL_ATTRS)


class TestValidateFrameworkAttributes:
    """Tests for validate_framework_attributes."""

    def test_valid_attrs_returns_empty(self):
        attrs = {"framework.name": "langchain", "framework.version": "0.3.0"}
        assert validate_framework_attributes(attrs) == []

    def test_missing_version_returns_it(self):
        attrs = {"framework.name": "langchain"}
        missing = validate_framework_attributes(attrs)
        assert "framework.version" in missing

    def test_missing_name_returns_it(self):
        attrs = {"framework.version": "1.0"}
        missing = validate_framework_attributes(attrs)
        assert "framework.name" in missing

    def test_empty_dict_returns_all_required(self):
        missing = validate_framework_attributes({})
        assert set(missing) == _REQUIRED_ATTRS

    def test_extra_attrs_dont_cause_failure(self):
        attrs = {
            "framework.name": "crewai",
            "framework.version": "0.30.0",
            "framework.crewai.extra": "value",
        }
        assert validate_framework_attributes(attrs) == []


class TestCreateFrameworkAttributes:
    """Tests for create_framework_attributes with auto-version detection."""

    def test_explicit_version_used(self):
        attrs = create_framework_attributes("crewai", "0.30.0")
        assert attrs["framework.name"] == "crewai"
        assert attrs["framework.version"] == "0.30.0"

    def test_auto_detect_version_when_none(self):
        with patch(
            "risicare.integrations._base.get_framework_version",
            return_value="1.2.3",
        ):
            attrs = create_framework_attributes("langchain")
            assert attrs["framework.version"] == "1.2.3"

    def test_no_version_if_undetectable(self):
        with patch(
            "risicare.integrations._base.get_framework_version",
            return_value=None,
        ):
            attrs = create_framework_attributes("langchain")
            assert "framework.version" not in attrs

    def test_extra_attrs_namespaced(self):
        attrs = create_framework_attributes("crewai", "0.30.0", crew_name="test")
        assert attrs["framework.crewai.crew_name"] == "test"

    def test_none_extras_excluded(self):
        attrs = create_framework_attributes("crewai", "0.30.0", crew_name=None)
        assert "framework.crewai.crew_name" not in attrs

    def test_passes_schema_validation(self):
        attrs = create_framework_attributes("langchain", "0.3.0")
        assert validate_framework_attributes(attrs) == []
