"""Tests for test execution runner."""

import pytest
from opencypher_compliance.adapters import QueryResult
from opencypher_compliance.runner import run_compliance, _run_test


class TestRunTest:
    def test_simple_pass(self, mock_adapter):
        mock_adapter.results["RETURN 1 AS x"] = QueryResult(columns=["x"], rows=[[1]])
        test = {"name": "RETURN", "type": "clause", "query": "RETURN 1 AS x",
                "expected_columns": ["x"], "expected_rows": [[1]]}
        result = _run_test(mock_adapter, test)
        assert result["result"] == "pass"
        assert result["element"] == "RETURN"
        assert "duration_ms" in result

    def test_simple_fail(self, mock_adapter):
        mock_adapter.results["RETURN 1 AS x"] = QueryResult(columns=["x"], rows=[[2]])
        test = {"name": "RETURN", "type": "clause", "query": "RETURN 1 AS x",
                "expected_columns": ["x"], "expected_rows": [[1]]}
        result = _run_test(mock_adapter, test)
        assert result["result"] == "fail"
        assert "error" in result

    def test_setup_and_teardown(self, mock_adapter):
        mock_adapter.results["MATCH (n) RETURN n.name AS name"] = QueryResult(columns=["name"], rows=[["Alice"]])
        test = {
            "name": "MATCH", "type": "clause",
            "setup": ["CREATE (:Person {name: 'Alice'})"],
            "query": "MATCH (n) RETURN n.name AS name",
            "expected_columns": ["name"], "expected_rows": [["Alice"]],
            "teardown": ["MATCH (n) DETACH DELETE n"],
        }
        result = _run_test(mock_adapter, test)
        assert result["result"] == "pass"
        assert "CREATE (:Person {name: 'Alice'})" in mock_adapter.executed_queries
        assert "MATCH (n) DETACH DELETE n" in mock_adapter.executed_queries

    def test_setup_failure(self, mock_adapter):
        mock_adapter.errors["CREATE (:Fail)"] = RuntimeError("setup boom")
        test = {
            "name": "FAIL", "type": "clause",
            "setup": ["CREATE (:Fail)"],
            "query": "RETURN 1",
            "expect_success": True,
        }
        result = _run_test(mock_adapter, test)
        assert result["result"] == "fail"
        assert "Setup error" in result["error"]

    def test_teardown_failure_triggers_cleanup(self, mock_adapter):
        mock_adapter.results["RETURN 1 AS x"] = QueryResult(columns=["x"], rows=[[1]])
        mock_adapter.errors["MATCH (n) DELETE n"] = RuntimeError("teardown boom")
        test = {
            "name": "T", "type": "clause",
            "query": "RETURN 1 AS x",
            "expected_columns": ["x"], "expected_rows": [[1]],
            "teardown": ["MATCH (n) DELETE n"],
        }
        result = _run_test(mock_adapter, test)
        assert result["result"] == "pass"  # test itself passed
        assert mock_adapter.cleanup_count >= 1  # cleanup called due to teardown failure

    def test_expect_error(self, mock_adapter):
        mock_adapter.errors["MANDATORY MATCH (n:Dog) RETURN n"] = RuntimeError("not supported")
        test = {
            "name": "MANDATORY MATCH", "type": "clause",
            "query": "MANDATORY MATCH (n:Dog) RETURN n",
            "expect_error": True,
        }
        result = _run_test(mock_adapter, test)
        assert result["result"] == "pass"

    def test_query_error_without_expect(self, mock_adapter):
        mock_adapter.errors["BAD QUERY"] = RuntimeError("syntax error")
        test = {"name": "BAD", "type": "clause", "query": "BAD QUERY", "expect_success": True}
        result = _run_test(mock_adapter, test)
        assert result["result"] == "fail"
        assert "Query error" in result["error"]

    def test_timing(self, mock_adapter):
        mock_adapter.results["RETURN 1"] = QueryResult()
        test = {"name": "T", "type": "clause", "query": "RETURN 1", "expect_success": True}
        result = _run_test(mock_adapter, test)
        assert isinstance(result["duration_ms"], int)
        assert result["duration_ms"] >= 0


class TestRunComplianceConnectionFailure:
    def test_connect_failure_raises(self, sample_config, tmp_path, monkeypatch):
        """If adapter.connect() fails, runner raises ConnectionError."""
        import yaml
        import opencypher_compliance.runner as runner_mod
        from opencypher_compliance.adapters import CypherAdapter

        class FailAdapter(CypherAdapter):
            def connect(self, host, port, auth, **options):
                raise OSError("Connection refused")
            def execute(self, query):
                raise NotImplementedError
            def close(self):
                pass

        monkeypatch.setattr(runner_mod, "get_adapter", lambda name: FailAdapter())

        catalog = {"tests": [{"name": "T", "type": "clause", "query": "RETURN 1", "expect_success": True}]}
        (tmp_path / "test.yaml").write_text(yaml.dump(catalog))

        with pytest.raises(ConnectionError, match="Cannot connect"):
            run_compliance(
                config=sample_config,
                catalog_dir=str(tmp_path),
            )

    def test_probe_failure_raises(self, sample_config, tmp_path, monkeypatch):
        """If probe query fails after connect(), runner raises ConnectionError."""
        import yaml
        import opencypher_compliance.runner as runner_mod
        from opencypher_compliance.adapters import CypherAdapter

        class ProbeFailAdapter(CypherAdapter):
            def connect(self, host, port, auth, **options):
                pass  # connect succeeds
            def execute(self, query):
                raise OSError("Connection reset")
            def close(self):
                pass

        monkeypatch.setattr(runner_mod, "get_adapter", lambda name: ProbeFailAdapter())

        catalog = {"tests": [{"name": "T", "type": "clause", "query": "RETURN 1", "expect_success": True}]}
        (tmp_path / "test.yaml").write_text(yaml.dump(catalog))

        with pytest.raises(ConnectionError, match="Cannot connect"):
            run_compliance(
                config=sample_config,
                catalog_dir=str(tmp_path),
            )

    def test_probe_timeout_raises(self, sample_config, tmp_path, monkeypatch):
        """If probe query hangs, runner raises ConnectionError after timeout."""
        import yaml
        import opencypher_compliance.runner as runner_mod
        from opencypher_compliance.adapters import CypherAdapter

        class HangingAdapter(CypherAdapter):
            def connect(self, host, port, auth, **options):
                pass
            def execute(self, query):
                import threading
                threading.Event().wait()  # hang forever
            def close(self):
                pass

        monkeypatch.setattr(runner_mod, "get_adapter", lambda name: HangingAdapter())
        monkeypatch.setattr(runner_mod, "PROBE_TIMEOUT_SEC", 1)

        catalog = {"tests": [{"name": "T", "type": "clause", "query": "RETURN 1", "expect_success": True}]}
        (tmp_path / "test.yaml").write_text(yaml.dump(catalog))

        with pytest.raises(ConnectionError, match="timed out"):
            run_compliance(
                config=sample_config,
                catalog_dir=str(tmp_path),
            )


class TestRunCompliance:
    def test_full_lifecycle(self, mock_adapter, sample_config, tmp_path):
        # Create a small catalog
        import yaml
        catalog = {
            "tests": [
                {"name": "T1", "type": "clause", "query": "RETURN 1 AS x",
                 "expected_columns": ["x"], "expected_rows": [[1]]},
                {"name": "T2", "type": "operator", "query": "RETURN 2 + 3 AS result",
                 "expected_rows": [[5]]},
            ]
        }
        (tmp_path / "test.yaml").write_text(yaml.dump(catalog))

        mock_adapter.results["RETURN 1 AS x"] = QueryResult(columns=["x"], rows=[[1]])
        mock_adapter.results["RETURN 2 + 3 AS result"] = QueryResult(columns=["result"], rows=[[5]])

        output = run_compliance(
            config=sample_config,
            catalog_dir=str(tmp_path),
            adapter_instance=mock_adapter,
        )

        assert output["metadata"]["total_tests"] == 2
        assert output["metadata"]["passed"] == 2
        assert output["metadata"]["failed"] == 0
        assert output["metadata"]["pass_rate"] == "100.00%"
        assert len(output["results"]) == 2
        assert "clause" in output["summary_by_type"]
        assert "operator" in output["summary_by_type"]

    def test_fail_fast(self, mock_adapter, sample_config, tmp_path):
        import yaml
        catalog = {
            "tests": [
                {"name": "T1", "type": "clause", "query": "FAIL",
                 "expected_rows": [[1]]},
                {"name": "T2", "type": "clause", "query": "RETURN 1",
                 "expect_success": True},
            ]
        }
        (tmp_path / "test.yaml").write_text(yaml.dump(catalog))
        mock_adapter.errors["FAIL"] = RuntimeError("boom")

        output = run_compliance(
            config=sample_config,
            catalog_dir=str(tmp_path),
            adapter_instance=mock_adapter,
            fail_fast=True,
        )

        # Should stop after first failure
        assert len(output["results"]) == 1
        assert output["results"][0]["result"] == "fail"

    def test_filter_by_type(self, mock_adapter, sample_config, tmp_path):
        import yaml
        catalog = {
            "tests": [
                {"name": "T1", "type": "clause", "query": "RETURN 1", "expect_success": True},
                {"name": "T2", "type": "operator", "query": "RETURN 2", "expect_success": True},
            ]
        }
        (tmp_path / "test.yaml").write_text(yaml.dump(catalog))

        output = run_compliance(
            config=sample_config,
            catalog_dir=str(tmp_path),
            adapter_instance=mock_adapter,
            types=["clause"],
        )

        assert output["metadata"]["total_tests"] == 1
        assert output["results"][0]["element"] == "T1"

    def test_metadata_structure(self, mock_adapter, sample_config, tmp_path):
        import yaml
        catalog = {"tests": [{"name": "T", "type": "clause", "query": "RETURN 1", "expect_success": True}]}
        (tmp_path / "test.yaml").write_text(yaml.dump(catalog))

        output = run_compliance(
            config=sample_config,
            catalog_dir=str(tmp_path),
            adapter_instance=mock_adapter,
        )

        meta = output["metadata"]
        assert meta["database"] == "Test DB"
        assert meta["adapter"] == "bolt"
        assert meta["spec_version"] == "9"
        assert "timestamp" in meta
        assert meta["database_version"] == "MockDB/1.0"
