"""Tests for m4.core.backends.base module.

Tests cover:
- QueryResult dataclass
- Backend exceptions (BackendError, ConnectionError, etc.)
- Backend protocol interface
"""

import pandas as pd

from m4.core.backends.base import (
    Backend,
    BackendError,
    ConnectionError,
    QueryExecutionError,
    QueryResult,
    TableNotFoundError,
    sanitize_error_message,
)


class TestQueryResult:
    """Test QueryResult dataclass."""

    def test_success_result(self):
        """Test creating a successful query result."""
        df = pd.DataFrame({"col": [1, 2, 3]})
        result = QueryResult(dataframe=df, row_count=10)

        assert result.dataframe is not None
        assert len(result.dataframe) == 3
        assert result.row_count == 10
        assert result.truncated is False
        assert result.error is None
        assert result.success is True

    def test_truncated_result(self):
        """Test creating a truncated query result."""
        df = pd.DataFrame({"col": range(100)})
        result = QueryResult(dataframe=df, row_count=100, truncated=True)

        assert result.truncated is True
        assert result.success is True

    def test_error_result(self):
        """Test creating an error query result."""
        result = QueryResult(dataframe=None, error="Query failed")

        assert result.error == "Query failed"
        assert result.success is False

    def test_empty_result(self):
        """Test creating an empty query result."""
        result = QueryResult(dataframe=pd.DataFrame(), row_count=0)

        assert result.row_count == 0
        assert result.success is True


class TestBackendErrors:
    """Test backend exception classes."""

    def test_backend_error(self):
        """Test base BackendError."""
        error = BackendError("test error", backend="duckdb")

        assert str(error) == "test error"
        assert error.message == "test error"
        assert error.backend == "duckdb"
        assert error.recoverable is False

    def test_backend_error_recoverable(self):
        """Test BackendError with recoverable flag."""
        error = BackendError("test error", backend="bigquery", recoverable=True)

        assert error.recoverable is True

    def test_connection_error(self):
        """Test ConnectionError (always recoverable)."""
        error = ConnectionError("Connection failed", backend="duckdb")

        assert str(error) == "Connection failed"
        assert error.recoverable is True  # Always recoverable

    def test_table_not_found_error(self):
        """Test TableNotFoundError."""
        error = TableNotFoundError("patients", backend="duckdb")

        assert "patients" in str(error)
        assert error.table_name == "patients"
        assert error.recoverable is False

    def test_query_execution_error(self):
        """Test QueryExecutionError."""
        error = QueryExecutionError(
            "Syntax error",
            sql="SELECT * FORM patients",  # typo in FROM
            backend="duckdb",
        )

        assert str(error) == "Syntax error"
        assert error.sql == "SELECT * FORM patients"
        assert error.recoverable is False


class TestBackendProtocol:
    """Test Backend protocol structure."""

    def test_backend_is_runtime_checkable(self):
        """Test that Backend protocol is runtime checkable."""

        class MockBackend:
            name = "mock"

            def execute_query(self, sql, dataset):
                return QueryResult(dataframe=pd.DataFrame())

            def get_table_list(self, dataset):
                return []

            def get_table_info(self, table_name, dataset):
                return QueryResult(dataframe=pd.DataFrame())

            def get_sample_data(self, table_name, dataset, limit=3):
                return QueryResult(dataframe=pd.DataFrame())

            def get_backend_info(self, dataset):
                return "Mock backend"

        mock = MockBackend()
        assert isinstance(mock, Backend)

    def test_incomplete_backend_not_recognized(self):
        """Test that incomplete backends are not recognized."""

        class IncompleteBackend:
            # Missing required methods
            name = "incomplete"

        backend = IncompleteBackend()
        assert not isinstance(backend, Backend)


class TestSanitizeErrorMessage:
    """Tests for sanitize_error_message function."""

    # --- DuckDB-style errors (existing patterns) ---

    def test_table_not_found(self):
        """DuckDB-style 'no such table' error."""
        err = Exception("Catalog Error: Table 'foo' not found")
        msg = sanitize_error_message(err, "duckdb")
        assert "Table not found" in msg

    def test_no_such_table(self):
        """DuckDB-style 'no such table' error."""
        err = Exception("no such table: patients")
        msg = sanitize_error_message(err, "duckdb")
        assert "Table not found" in msg

    def test_column_not_found(self):
        """Column not found error."""
        err = Exception("no such column: foobar")
        msg = sanitize_error_message(err, "duckdb")
        assert "Column not found" in msg

    def test_syntax_error(self):
        """SQL syntax error."""
        err = Exception("Parser Error: syntax error at or near 'FORM'")
        msg = sanitize_error_message(err, "duckdb")
        assert "syntax error" in msg.lower()

    def test_timeout(self):
        """Query timeout."""
        err = Exception("Query timed out after 300s")
        msg = sanitize_error_message(err, "duckdb")
        assert "timed out" in msg.lower()

    def test_connection_error(self):
        """Connection error."""
        err = Exception("connection refused")
        msg = sanitize_error_message(err, "duckdb")
        assert "Connection error" in msg

    # --- BigQuery-specific errors (new patterns) ---

    def test_bigquery_404_dataset_not_found(self):
        """BigQuery 404 when dataset doesn't exist."""
        err = Exception("404 Not Found: Dataset my-project:my_dataset was not found")
        msg = sanitize_error_message(err, "bigquery")
        assert "Resource not found" in msg

    def test_bigquery_404_project_not_found(self):
        """BigQuery 404 when project doesn't exist."""
        err = Exception("404 Not found: Project bad-project was not found")
        msg = sanitize_error_message(err, "bigquery")
        assert "Resource not found" in msg

    def test_bigquery_billing_error(self):
        """BigQuery billing not enabled."""
        err = Exception(
            "Access Denied: BigQuery BigQuery: Billing is not enabled for this project"
        )
        msg = sanitize_error_message(err, "bigquery")
        assert "Billing error" in msg

    def test_bigquery_403_forbidden(self):
        """BigQuery 403 Forbidden."""
        err = Exception("403 Forbidden: Access denied for user@example.com")
        msg = sanitize_error_message(err, "bigquery")
        assert "Permission denied" in msg

    def test_bigquery_permission_denied(self):
        """BigQuery access denied."""
        err = Exception("Access Denied: permission denied for dataset")
        msg = sanitize_error_message(err, "bigquery")
        assert "Permission denied" in msg

    def test_bigquery_quota_exceeded(self):
        """BigQuery quota exceeded."""
        err = Exception("Quota exceeded: Too many concurrent queries")
        msg = sanitize_error_message(err, "bigquery")
        assert "Quota exceeded" in msg

    def test_bigquery_rate_limit(self):
        """BigQuery rate limit."""
        err = Exception("Rate limit exceeded for API calls")
        msg = sanitize_error_message(err, "bigquery")
        assert "rate limited" in msg.lower() or "Quota exceeded" in msg

    # --- Generic fallback ---

    def test_generic_fallback_includes_error_type(self):
        """Generic fallback includes the exception type name."""
        err = ValueError("some unexpected internal error")
        msg = sanitize_error_message(err, "bigquery")
        assert "Query execution failed" in msg
        assert "ValueError" in msg

    def test_generic_fallback_for_unknown_exception(self):
        """Generic fallback for completely unknown errors."""

        class CustomBQError(Exception):
            pass

        err = CustomBQError("something went wrong")
        msg = sanitize_error_message(err, "bigquery")
        assert "Query execution failed" in msg
        assert "CustomBQError" in msg

    # --- Additional edge cases and variant patterns ---

    def test_column_not_found_unknown_column(self):
        """MySQL-style 'Unknown column' error."""
        err = Exception("Unknown column 'xyz' in field list")
        msg = sanitize_error_message(err, "duckdb")
        assert "Column not found" in msg

    def test_parse_error(self):
        """Parse error variant of syntax error."""
        err = Exception("Parse error: unexpected token")
        msg = sanitize_error_message(err, "duckdb")
        assert "SQL syntax error" in msg

    def test_bigquery_access_denied(self):
        """BigQuery 'Access Denied' triggers permission denied."""
        err = Exception("Access Denied: Access is denied")
        msg = sanitize_error_message(err, "bigquery")
        assert "Permission denied" in msg

    def test_bigquery_unauthorized(self):
        """BigQuery 401 Unauthorized triggers permission denied."""
        err = Exception(
            "401 Unauthorized: Request had invalid authentication credentials"
        )
        msg = sanitize_error_message(err, "bigquery")
        assert "Permission denied" in msg

    def test_timed_out_variant(self):
        """'timed out' variant triggers timeout message."""
        err = Exception("Operation timed out")
        msg = sanitize_error_message(err, "duckdb")
        assert "timed out" in msg

    def test_network_error(self):
        """Network unreachable triggers connection error."""
        err = Exception("Network is unreachable")
        msg = sanitize_error_message(err, "duckdb")
        assert "Connection error" in msg

    def test_generic_error_truncates_long_message(self):
        """Generic fallback truncates messages longer than 200 characters."""
        err = Exception("x" * 300)
        msg = sanitize_error_message(err, "duckdb")
        assert len(msg) < 300
        assert msg.endswith("...")

    def test_billing_before_permission(self):
        """Billing check has priority over permission denied check."""
        err = Exception("Access Denied: Billing not enabled for project")
        msg = sanitize_error_message(err, "bigquery")
        assert "Billing error" in msg

    def test_backend_name_logged(self, caplog):
        """Backend name is included in debug log message."""
        import logging

        with caplog.at_level(logging.DEBUG, logger="m4"):
            sanitize_error_message(Exception("test error"), "bigquery")
        assert "[bigquery]" in caplog.text
