"""
Tests for llm-guard-kit v0.54.0

Covers:
- SQLSemanticValidator: heuristic fallback, dialect detection, batch
- SQLExecutionOracle: sqlite execution, check_raw, blend_with_behavioral
- ChartValidator: heuristic fallback, Vega-Lite spec checks
- AutoGenAdapter: message-to-steps conversion, score_from_messages
- CrewAIAdapter: task output conversion, score_from_task_output
- OrchestratorScoringResult: risk_score/confidence_tier properties
- P(True) judge-model staleness warning in score_with_ptrue()
- FailureTaxonomist: EXCESSIVE_SEARCH unlocked in non-experimental mode,
  new signals (answer_sim, obs_contradiction, is_multi_hop),
  ANSWER_UNSUPPORTED combined gate, CONFLICTING_EVIDENCE obs_contradiction
"""

from __future__ import annotations

import sqlite3
import warnings
from dataclasses import dataclass
from typing import Any, Dict, List
from unittest.mock import MagicMock, patch

import pytest


# ═══════════════════════════════════════════════════════════════════════════════
# SQL Validator
# ═══════════════════════════════════════════════════════════════════════════════

class TestSQLSemanticValidatorHeuristic:
    """Tests for the zero-cost heuristic fallback (no API key)."""

    def setup_method(self):
        from llm_guard import SQLSemanticValidator
        self.v = SQLSemanticValidator()  # no api_key → heuristic mode

    def test_valid_simple_select(self):
        result = self.v.validate(
            "Get all users",
            "SELECT * FROM users",
        )
        assert result.verdict == "VALID"
        assert result.risk_score == pytest.approx(0.10)

    def test_missing_group_by_detected(self):
        result = self.v.validate(
            "Total revenue by region",
            "SELECT region, SUM(amount) FROM orders",
        )
        assert result.verdict in ("UNCERTAIN", "INVALID")
        assert any("GROUP BY" in issue for issue in result.issues)

    def test_join_without_on_detected(self):
        result = self.v.validate(
            "Join orders and customers",
            "SELECT * FROM orders JOIN customers",
        )
        assert result.verdict in ("UNCERTAIN", "INVALID")
        assert any("JOIN" in issue for issue in result.issues)

    def test_join_with_using_ok(self):
        result = self.v.validate(
            "Join orders and customers",
            "SELECT * FROM orders JOIN customers USING (customer_id)",
        )
        assert result.verdict == "VALID"

    def test_databricks_merge_without_when_detected(self):
        result = self.v.validate(
            "Merge updates into target",
            "MERGE INTO target USING source ON target.id = source.id",
            dialect="databricks",
        )
        assert result.verdict in ("UNCERTAIN", "INVALID")
        assert any("MERGE" in issue for issue in result.issues)

    def test_databricks_merge_valid(self):
        result = self.v.validate(
            "Merge updates",
            "MERGE INTO target USING source ON target.id = source.id "
            "WHEN MATCHED THEN UPDATE SET col = source.col",
            dialect="databricks",
        )
        assert result.verdict == "VALID"

    def test_snowflake_qualify_without_window(self):
        result = self.v.validate(
            "Filter rows",
            "SELECT * FROM t QUALIFY row_num = 1",
            dialect="snowflake",
        )
        assert result.verdict in ("UNCERTAIN", "INVALID")
        assert any("QUALIFY" in issue for issue in result.issues)

    def test_snowflake_qualify_with_window_ok(self):
        result = self.v.validate(
            "Top row per group",
            "SELECT *, ROW_NUMBER() OVER (PARTITION BY grp ORDER BY val) AS rn "
            "FROM t QUALIFY rn = 1",
            dialect="snowflake",
        )
        assert result.verdict == "VALID"

    def test_spark_distribute_and_cluster_conflict(self):
        result = self.v.validate(
            "Partition data",
            "SELECT * FROM t DISTRIBUTE BY key CLUSTER BY key",
            dialect="spark",
        )
        assert result.verdict in ("UNCERTAIN", "INVALID")
        assert any("DISTRIBUTE BY" in issue and "CLUSTER BY" in issue for issue in result.issues)

    def test_dialect_note_in_result(self):
        result = self.v.validate("Q", "SELECT 1", dialect="bigquery")
        assert result.dialect == "bigquery"

    def test_no_schema_warning(self):
        with warnings.catch_warnings(record=True) as ws:
            warnings.simplefilter("always")
            self.v.validate("Q", "SELECT * FROM t")
        assert any("no schema" in str(w.message).lower() for w in ws)

    def test_batch_validate(self):
        pairs = [
            {"question": "All users", "sql": "SELECT * FROM users"},
            {"question": "Revenue by region",
             "sql": "SELECT region, SUM(amount) FROM orders"},
        ]
        results = self.v.validate_batch(pairs)
        assert len(results) == 2
        assert all(hasattr(r, "verdict") for r in results)

    def test_batch_per_item_dialect(self):
        pairs = [
            {"question": "Q1", "sql": "SELECT 1", "dialect": "databricks"},
            {"question": "Q2", "sql": "SELECT 1", "dialect": "bigquery"},
        ]
        results = self.v.validate_batch(pairs)
        assert results[0].dialect == "databricks"
        assert results[1].dialect == "bigquery"

    def test_result_fields(self):
        from llm_guard import SQLValidationResult
        result = self.v.validate("Q", "SELECT * FROM t", schema="CREATE TABLE t (id INT)")
        assert isinstance(result, SQLValidationResult)
        assert result.verdict in ("VALID", "UNCERTAIN", "INVALID")
        assert 0.0 <= result.risk_score <= 1.0
        assert isinstance(result.issues, list)
        assert result.judge_model == "heuristic"


class TestSQLDialectsExported:
    def test_sql_dialects_set_exported(self):
        from llm_guard import SQL_DIALECTS
        assert "databricks" in SQL_DIALECTS
        assert "bigquery" in SQL_DIALECTS
        assert "snowflake" in SQL_DIALECTS
        assert "spark" in SQL_DIALECTS
        assert "postgres" in SQL_DIALECTS
        assert "ansi" in SQL_DIALECTS


class TestSQLExecutionOracle:
    def _make_oracle(self, db_path=":memory:"):
        from llm_guard import SQLExecutionOracle
        conn = sqlite3.connect(db_path)
        conn.execute("CREATE TABLE IF NOT EXISTS t (id INT, val TEXT)")
        conn.execute("INSERT INTO t VALUES (1, 'a'), (2, 'b')")
        conn.commit()
        conn.close()
        return SQLExecutionOracle(
            connection_factory=lambda: sqlite3.connect(db_path),
            dialect="ansi",
        )

    def test_successful_execution(self, tmp_path):
        db = str(tmp_path / "test.db")
        oracle = self._make_oracle(db)
        result = oracle.check("SELECT * FROM t")
        assert result.passed is True
        assert result.shape == (2, 2)
        assert result.execution_risk == pytest.approx(0.0)
        assert result.error is None

    def test_empty_result_risk(self, tmp_path):
        db = str(tmp_path / "test.db")
        oracle = self._make_oracle(db)
        result = oracle.check("SELECT * FROM t WHERE val = 'nonexistent'")
        assert result.passed is True
        assert result.shape[0] == 0
        assert result.execution_risk == pytest.approx(0.50)

    def test_syntax_error_risk(self, tmp_path):
        db = str(tmp_path / "test.db")
        oracle = self._make_oracle(db)
        result = oracle.check("THIS IS NOT SQL")
        assert result.passed is False
        assert result.execution_risk == pytest.approx(0.90)
        assert result.error is not None

    def test_shape_mismatch_fails(self, tmp_path):
        db = str(tmp_path / "test.db")
        oracle = self._make_oracle(db)
        result = oracle.check("SELECT * FROM t", expected_shape=(1, 2))
        assert result.passed is False  # got (2,2) not (1,2)

    def test_blend_with_behavioral(self, tmp_path):
        db = str(tmp_path / "test.db")
        oracle = self._make_oracle(db)
        result = oracle.check("SELECT * FROM t")
        blended = oracle.blend_with_behavioral(result, behavioral_risk=0.6, oracle_weight=0.4)
        # 0.4 * 0.0 + 0.6 * 0.6 = 0.36
        assert blended == pytest.approx(0.36, abs=0.01)

    def test_check_raw_list_result(self, tmp_path):
        db = str(tmp_path / "test.db")
        oracle = self._make_oracle(db)
        rows = [(1, "a"), (2, "b")]
        result = oracle.check_raw("SELECT * FROM t", result=rows)
        assert result.passed is True
        assert result.execution_risk == pytest.approx(0.0)

    def test_check_raw_error(self, tmp_path):
        db = str(tmp_path / "test.db")
        oracle = self._make_oracle(db)
        result = oracle.check_raw("SELECT * FROM t", result=None, error="syntax error")
        assert result.passed is False
        assert result.execution_risk == pytest.approx(0.90)

    def test_check_raw_empty(self, tmp_path):
        db = str(tmp_path / "test.db")
        oracle = self._make_oracle(db)
        result = oracle.check_raw("SELECT * FROM t", result=[])
        assert result.execution_risk == pytest.approx(0.50)


# ═══════════════════════════════════════════════════════════════════════════════
# Chart Validator
# ═══════════════════════════════════════════════════════════════════════════════

class TestChartValidatorHeuristic:
    """Tests for the zero-cost heuristic fallback (no API key)."""

    def setup_method(self):
        from llm_guard import ChartValidator
        self.v = ChartValidator()  # no api_key → heuristic mode

    def test_valid_bar_comparison(self):
        result = self.v.validate(
            "Compare revenue by region",
            {"mark": "bar", "encoding": {"x": {"field": "region"}, "y": {"field": "revenue"}}},
            data_sample=[{"region": "NA", "revenue": 100}, {"region": "EU", "revenue": 200}],
        )
        assert result.verdict == "VALID"
        assert result.risk_score == pytest.approx(0.10)

    def test_wrong_chart_type_for_trend(self):
        result = self.v.validate(
            "Show revenue trend over time",
            {"mark": "bar", "encoding": {"x": {"field": "month"}, "y": {"field": "revenue"}}},
        )
        assert result.verdict in ("UNCERTAIN", "INVALID")
        assert any("trend" in issue.lower() or "time" in issue.lower() for issue in result.issues)

    def test_scatter_for_correlation_ok(self):
        result = self.v.validate(
            "Show correlation between age and income",
            {"mark": "point", "encoding": {"x": {"field": "age"}, "y": {"field": "income"}}},
        )
        assert result.verdict == "VALID"

    def test_missing_encoding_fields(self):
        result = self.v.validate(
            "Show data",
            {"mark": "bar", "encoding": {"x": {}, "y": {}}},
        )
        assert result.verdict in ("UNCERTAIN", "INVALID")
        assert any("field" in issue.lower() for issue in result.issues)

    def test_field_not_in_data_sample(self):
        result = self.v.validate(
            "Show revenue by region",
            {"mark": "bar", "encoding": {
                "x": {"field": "region"}, "y": {"field": "nonexistent_col"}
            }},
            data_sample=[{"region": "NA", "revenue": 100}],
        )
        assert result.verdict in ("UNCERTAIN", "INVALID")
        assert any("nonexistent_col" in issue for issue in result.issues)

    def test_string_spec_detected(self):
        result = self.v.validate(
            "Show correlation between X and Y",
            "scatter plot of x vs y",
        )
        assert result.verdict == "VALID"

    def test_batch_validate(self):
        items = [
            {
                "question": "Revenue comparison",
                "chart_spec": {"mark": "bar"},
                "data_sample": [{"rev": 1}],
            },
            {
                "question": "Trend over time",
                "chart_spec": {"mark": "line"},
            },
        ]
        results = self.v.validate_batch(items)
        assert len(results) == 2

    def test_result_fields(self):
        from llm_guard import ChartValidationResult
        result = self.v.validate("Q", {"mark": "bar"})
        assert isinstance(result, ChartValidationResult)
        assert result.verdict in ("VALID", "UNCERTAIN", "INVALID")
        assert 0.0 <= result.risk_score <= 1.0
        assert isinstance(result.issues, list)
        assert result.judge_model == "heuristic"


# ═══════════════════════════════════════════════════════════════════════════════
# Orchestrator Adapters
# ═══════════════════════════════════════════════════════════════════════════════

def _make_guard():
    """Create an AgentGuard with no API key (behavioral only)."""
    from llm_guard import AgentGuard
    return AgentGuard()


class TestAutoGenAdapter:
    def test_score_from_empty_messages_warns(self):
        from llm_guard import AutoGenAdapter
        adapter = AutoGenAdapter(_make_guard())
        with warnings.catch_warnings(record=True) as ws:
            warnings.simplefilter("always")
            result = adapter.score_from_messages(
                [], question="What is the capital of France?", final_answer="Paris"
            )
        assert any("empty" in str(w.message).lower() for w in ws)
        assert 0.0 <= result.risk_score <= 1.0

    def test_score_from_plain_messages(self):
        from llm_guard import AutoGenAdapter
        adapter = AutoGenAdapter(_make_guard())
        msgs = [
            {"role": "user", "content": "What is the capital of France?"},
            {"role": "assistant", "content": "Let me search for this."},
            {"role": "assistant", "content": "The capital of France is Paris."},
        ]
        result = adapter.score_from_messages(msgs, question="What is the capital of France?")
        assert result.framework == "autogen"
        assert result.n_messages == 3
        assert result.n_steps >= 1
        assert 0.0 <= result.risk_score <= 1.0

    def test_score_from_tool_call_messages(self):
        from llm_guard import AutoGenAdapter
        adapter = AutoGenAdapter(_make_guard())
        msgs = [
            {"role": "user", "content": "What year was Python created?"},
            {
                "role": "assistant",
                "content": None,
                "tool_calls": [{"function": {"name": "search", "arguments": '{"q": "Python language creation year"}'}}],
            },
            {"role": "tool", "content": "Python was created in 1991 by Guido van Rossum."},
            {"role": "assistant", "content": "Python was created in 1991."},
        ]
        result = adapter.score_from_messages(
            msgs, question="When was Python created?", final_answer="1991"
        )
        assert result.n_steps >= 2
        assert result.framework == "autogen"

    def test_result_properties(self):
        from llm_guard import AutoGenAdapter, OrchestratorScoringResult
        adapter = AutoGenAdapter(_make_guard())
        msgs = [
            {"role": "user", "content": "Q"},
            {"role": "assistant", "content": "A"},
        ]
        result = adapter.score_from_messages(msgs, question="Q", final_answer="A")
        assert isinstance(result, OrchestratorScoringResult)
        assert hasattr(result, "risk_score")
        assert hasattr(result, "confidence_tier")
        assert hasattr(result, "needs_alert")
        assert result.confidence_tier in ("HIGH", "MEDIUM", "LOW")

    def test_to_trust_object(self):
        from llm_guard import AutoGenAdapter, A2ATrustObject
        adapter = AutoGenAdapter(_make_guard())
        msgs = [{"role": "assistant", "content": "Paris"}]
        result = adapter.score_from_messages(msgs, question="Capital?", final_answer="Paris")
        trust = adapter.to_trust_object(result, answer="Paris")
        assert isinstance(trust, A2ATrustObject)
        assert trust.answer == "Paris"
        assert trust.routing_mode == "autogen_adapter"


class TestCrewAIAdapter:
    def test_score_from_dict_output(self):
        from llm_guard import CrewAIAdapter
        adapter = CrewAIAdapter(_make_guard())
        task_output = {"output": "Paris is the capital of France."}
        result = adapter.score_from_task_output(
            task_output, question="What is the capital of France?"
        )
        assert result.framework == "crewai"
        assert 0.0 <= result.risk_score <= 1.0

    def test_score_from_string_output(self):
        from llm_guard import CrewAIAdapter
        adapter = CrewAIAdapter(_make_guard())
        result = adapter.score_from_task_output(
            "The answer is 42.", question="What is the answer?"
        )
        assert result.framework == "crewai"
        assert result.final_answer == "The answer is 42."

    def test_score_from_object_with_raw(self):
        from llm_guard import CrewAIAdapter

        class FakeTaskOutput:
            raw = "Python was created in 1991."
            agent = "researcher"
            pydantic = None
            json_dict = None

        adapter = CrewAIAdapter(_make_guard())
        result = adapter.score_from_task_output(
            FakeTaskOutput(), question="When was Python created?"
        )
        assert result.framework == "crewai"
        assert "1991" in result.final_answer

    def test_empty_output_warns(self):
        from llm_guard import CrewAIAdapter

        class EmptyOutput:
            pass

        adapter = CrewAIAdapter(_make_guard())
        with warnings.catch_warnings(record=True) as ws:
            warnings.simplefilter("always")
            result = adapter.score_from_task_output(EmptyOutput(), question="Q")
        # Should not raise — should return a result
        assert result.framework == "crewai"

    def test_to_trust_object(self):
        from llm_guard import CrewAIAdapter, A2ATrustObject
        adapter = CrewAIAdapter(_make_guard())
        result = adapter.score_from_task_output("Answer", question="Q")
        trust = adapter.to_trust_object(result)
        assert isinstance(trust, A2ATrustObject)
        assert trust.routing_mode == "crewai_adapter"

    def test_crew_level_output(self):
        """Test scoring a Crew-level output with multiple task outputs."""
        from llm_guard import CrewAIAdapter

        class FakeTaskOut:
            raw = "Paris"
            agent = "researcher"
            pydantic = None
            json_dict = None

        class FakeCrewOutput:
            tasks_output = [FakeTaskOut(), FakeTaskOut()]
            raw = "Final: Paris"

        adapter = CrewAIAdapter(_make_guard())
        result = adapter.score_from_task_output(FakeCrewOutput(), question="Capital of France?")
        assert result.framework == "crewai"
        assert result.n_messages == 2


# ═══════════════════════════════════════════════════════════════════════════════
# P(True) judge-model staleness warning
# ═══════════════════════════════════════════════════════════════════════════════

class TestPTrueJudgeStalenessWarning:
    def test_stale_judge_model_warns_in_score_with_ptrue(self):
        """score_with_ptrue() should warn when judge_model changed since calibration."""
        from llm_guard import AgentGuard

        guard = AgentGuard(judge_model="claude-sonnet-4-6")
        # Simulate a calibration that was fitted with the old judge model
        guard._calibrated_judge_model = "claude-opus-4-6"

        steps = [
            {"thought": "searching", "action_type": "Search",
             "action_arg": "France capital", "observation": "Paris is the capital"},
            {"thought": "done", "action_type": "Finish",
             "action_arg": "Paris", "observation": ""},
        ]

        with warnings.catch_warnings(record=True) as ws:
            warnings.simplefilter("always")
            # No API key → will return behavioral-only but still check staleness
            guard.score_with_ptrue("What is the capital of France?", steps, "Paris")

        assert any(
            "calibrat" in str(w.message).lower() and "judge" in str(w.message).lower()
            for w in ws
        ), "Expected stale-judge-model UserWarning from score_with_ptrue()"

    def test_no_warning_when_judge_model_unchanged(self):
        from llm_guard import AgentGuard

        guard = AgentGuard(judge_model="claude-sonnet-4-6")
        guard._calibrated_judge_model = "claude-sonnet-4-6"  # same model

        steps = [
            {"thought": "searching", "action_type": "Search",
             "action_arg": "Python", "observation": "Python is a language"},
            {"thought": "done", "action_type": "Finish",
             "action_arg": "Python", "observation": ""},
        ]

        with warnings.catch_warnings(record=True) as ws:
            warnings.simplefilter("always")
            guard.score_with_ptrue("Python?", steps, "Python")

        judge_stale = [w for w in ws
                       if "judge" in str(w.message).lower() and "calibrat" in str(w.message).lower()
                       and "stale" not in str(w.message).lower()]
        # The new warning specifically says "current judge_model='..." so check for absence of
        # the "judge_model=" mismatch pattern
        mismatch_warns = [
            w for w in ws
            if "judge_model" in str(w.message) and "current" in str(w.message)
        ]
        assert len(mismatch_warns) == 0


# ═══════════════════════════════════════════════════════════════════════════════
# FailureTaxonomist v0.54 improvements
# ═══════════════════════════════════════════════════════════════════════════════

class TestFailureTaxonomistV054:
    def setup_method(self):
        from qppg_service.failure_taxonomy import FailureTaxonomist
        self.tx_prod = FailureTaxonomist(experimental=False)
        self.tx_exp  = FailureTaxonomist(experimental=True)

    def test_excessive_search_unlocked_production(self):
        """EXCESSIVE_SEARCH should now be available in non-experimental mode."""
        steps = [
            {"thought": f"searching attempt {i}", "action_type": "Search",
             "action_arg": f"query {i}", "observation": f"result {i}"}
            for i in range(6)
        ] + [{"thought": "done", "action_type": "Finish", "action_arg": "answer", "observation": ""}]

        result = self.tx_prod.classify("Q?", steps, "answer", finished=True)
        # Should NOT collapse to "OTHER" now that EXCESSIVE_SEARCH is validated
        assert result.primary_mode in ("EXCESSIVE_SEARCH", "RETRIEVAL_FAILURE", "OTHER"), \
            f"Unexpected mode: {result.primary_mode}"
        # At minimum it should detect EXCESSIVE_SEARCH in experimental mode
        result_exp = self.tx_exp.classify("Q?", steps, "answer", finished=True)
        assert "EXCESSIVE_SEARCH" in result_exp.all_modes

    def test_new_signals_present(self):
        """answer_sim, obs_contradiction, is_multi_hop should be in signals dict."""
        steps = [
            {"thought": "t", "action_type": "Search", "action_arg": "q", "observation": "obs"}
        ]
        result = self.tx_exp.classify("What happened after the event?", steps, "ans")
        assert "answer_sim" in result.signals
        assert "obs_contradiction" in result.signals
        assert "is_multi_hop" in result.signals

    def test_multi_hop_single_step_premature_stop(self):
        """Multi-hop question + single search should trigger PREMATURE_STOP."""
        steps = [
            {"thought": "searching", "action_type": "Search",
             "action_arg": "something", "observation": "some result"},
            {"thought": "done", "action_type": "Finish",
             "action_arg": "answer", "observation": ""},
        ]
        result = self.tx_exp.classify(
            "Compare the effect of A on B after C happened",
            steps, "answer", finished=True
        )
        assert "PREMATURE_STOP" in result.all_modes

    def test_zero_search_premature_stop(self):
        """Zero searches + finished → PREMATURE_STOP."""
        steps = [
            {"thought": "I know this", "action_type": "Finish",
             "action_arg": "Paris", "observation": ""},
        ]
        result = self.tx_exp.classify("What is the capital of France?", steps, "Paris", finished=True)
        assert "PREMATURE_STOP" in result.all_modes

    def test_answer_supported_via_embedding(self):
        """When answer is clearly in the reasoning, ANSWER_UNSUPPORTED should NOT fire."""
        # Construct a chain where answer matches observations closely
        obs = "Paris is the capital of France. The Eiffel Tower is in Paris."
        steps = [
            {"thought": "searching for France capital", "action_type": "Search",
             "action_arg": "France capital city", "observation": obs},
            {"thought": "Paris is confirmed", "action_type": "Finish",
             "action_arg": "Paris", "observation": ""},
        ]
        result = self.tx_exp.classify("What is the capital of France?", steps, "Paris")
        assert "ANSWER_UNSUPPORTED" not in result.all_modes

    def test_low_risk_for_clean_chain(self):
        """A well-formed 2-step chain on a factoid question should classify as LOW_RISK."""
        obs = "Paris is the capital and most populous city of France."
        steps = [
            {"thought": "let me search", "action_type": "Search",
             "action_arg": "capital of France", "observation": obs},
            {"thought": "found it", "action_type": "Finish",
             "action_arg": "Paris", "observation": ""},
        ]
        result = self.tx_prod.classify("What is the capital of France?", steps, "Paris")
        # In production mode, should not flag this as a failure
        assert result.primary_mode in ("LOW_RISK", "RETRIEVAL_FAILURE")

    def test_excessive_search_primary_in_production_when_many_steps(self):
        """Many search steps should yield EXCESSIVE_SEARCH in production mode."""
        steps = [
            {"thought": f"search {i}", "action_type": "Search",
             "action_arg": f"q{i}",
             "observation": "The capital of France is Paris. This is well known."}
            for i in range(7)
        ] + [{"thought": "done", "action_type": "Finish", "action_arg": "Paris", "observation": ""}]
        result = self.tx_prod.classify("What is the capital of France?", steps, "Paris")
        # High-relevance observations + many steps → EXCESSIVE_SEARCH not RETRIEVAL_FAILURE
        assert result.primary_mode in ("EXCESSIVE_SEARCH", "RETRIEVAL_FAILURE")


# ═══════════════════════════════════════════════════════════════════════════════
# Import smoke test
# ═══════════════════════════════════════════════════════════════════════════════

class TestImports:
    def test_all_v054_exports_importable(self):
        from llm_guard import (
            SQLSemanticValidator,
            SQLValidationResult,
            SQLExecutionOracle,
            SQLOracleResult,
            SQL_DIALECTS,
            ChartValidator,
            ChartValidationResult,
            AutoGenAdapter,
            CrewAIAdapter,
            OrchestratorScoringResult,
        )

    def test_version_bumped(self):
        import llm_guard
        assert llm_guard.__version__ >= "0.54.0"
