"""
Tests for pipeline dashboard: DB model, API routes, and ETL persistence.
"""

from __future__ import annotations

import uuid
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch

import pytest
from sqlalchemy.orm import Session, sessionmaker

# Suppress pre-existing pydantic deprecation warning from pycharter.api.dependencies.store
pytestmark = pytest.mark.filterwarnings(
    "ignore:Support for class-based `config` is deprecated:DeprecationWarning"
)

from pycharter.db.models.base import get_engine
from pycharter.db.models.pipeline_run import PipelineRunModel
from pycharter.etl_generator.result import BatchResult, PipelineResult

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def db_session():
    """Create an in-memory SQLite session with pipeline_runs table."""
    # get_engine adds event listeners that strip 'pycharter.' schema prefix for SQLite
    engine = get_engine("sqlite:///:memory:")

    # Only create the pipeline_runs table (avoids JSONB issues with other models)
    table = PipelineRunModel.__table__
    original_schema = table.schema
    table.schema = None
    table.create(engine)
    table.schema = original_schema

    session_factory = sessionmaker(bind=engine)
    session = session_factory()
    yield session
    session.close()


def _make_run(
    run_id: str = "abc12345",
    pipeline_name: str = "test-pipeline",
    status: str = "success",
    rows_extracted: int = 100,
    rows_loaded: int = 95,
    rows_failed: int = 5,
    duration_seconds: float = 2.5,
    started_at: datetime | None = None,
) -> PipelineRunModel:
    """Create a PipelineRunModel instance."""
    return PipelineRunModel(
        id=uuid.uuid4(),
        run_id=run_id,
        pipeline_name=pipeline_name,
        status=status,
        rows_extracted=rows_extracted,
        rows_transformed=rows_extracted,
        rows_loaded=rows_loaded,
        rows_failed=rows_failed,
        rows_quarantined=0,
        batches_processed=1,
        started_at=started_at or datetime.now(timezone.utc),
        completed_at=datetime.now(timezone.utc),
        duration_seconds=duration_seconds,
        trigger="api",
    )


# ---------------------------------------------------------------------------
# TestPipelineRunModel
# ---------------------------------------------------------------------------


class TestPipelineRunModel:
    """Tests for PipelineRunModel database operations."""

    def test_create_and_query(self, db_session: Session):
        """Test creating and querying a pipeline run."""
        run = _make_run()
        db_session.add(run)
        db_session.commit()

        result = db_session.query(PipelineRunModel).first()
        assert result is not None
        assert result.run_id == "abc12345"
        assert result.pipeline_name == "test-pipeline"
        assert result.status == "success"
        assert result.rows_loaded == 95

    def test_unique_run_id(self, db_session: Session):
        """Test that run_id is unique."""
        run1 = _make_run(run_id="dup12345")
        run2 = _make_run(run_id="dup12345")
        db_session.add(run1)
        db_session.commit()

        db_session.add(run2)
        with pytest.raises(Exception):
            db_session.commit()
        db_session.rollback()

    def test_filter_by_status(self, db_session: Session):
        """Test filtering by status."""
        db_session.add(_make_run(run_id="run1", status="success"))
        db_session.add(_make_run(run_id="run2", status="failed"))
        db_session.add(_make_run(run_id="run3", status="success"))
        db_session.commit()

        successful = (
            db_session.query(PipelineRunModel)
            .filter(PipelineRunModel.status == "success")
            .all()
        )
        assert len(successful) == 2

        failed = (
            db_session.query(PipelineRunModel)
            .filter(PipelineRunModel.status == "failed")
            .all()
        )
        assert len(failed) == 1

    def test_filter_by_pipeline_name(self, db_session: Session):
        """Test filtering by pipeline name."""
        db_session.add(_make_run(run_id="run1", pipeline_name="orders"))
        db_session.add(_make_run(run_id="run2", pipeline_name="users"))
        db_session.add(_make_run(run_id="run3", pipeline_name="orders"))
        db_session.commit()

        orders = (
            db_session.query(PipelineRunModel)
            .filter(PipelineRunModel.pipeline_name == "orders")
            .all()
        )
        assert len(orders) == 2

    def test_json_fields(self, db_session: Session):
        """Test JSON columns store and retrieve correctly."""
        run = _make_run()
        run.errors = ["Connection timeout", "Retry failed"]
        run.batch_results = [
            {"batch_index": 0, "rows_in": 100, "rows_out": 95},
        ]
        run.config_snapshot = {
            "extract": {"type": "http"},
            "load": {"type": "postgres"},
        }
        db_session.add(run)
        db_session.commit()

        result = db_session.query(PipelineRunModel).first()
        assert result is not None
        assert result.errors == ["Connection timeout", "Retry failed"]
        assert result.batch_results[0]["rows_in"] == 100
        assert result.config_snapshot["extract"]["type"] == "http"

    def test_nullable_fields(self, db_session: Session):
        """Test that nullable fields default correctly."""
        run = PipelineRunModel(
            id=uuid.uuid4(),
            run_id="min12345",
            status="success",
        )
        db_session.add(run)
        db_session.commit()

        result = db_session.query(PipelineRunModel).first()
        assert result is not None
        assert result.pipeline_name is None
        assert result.contract_quality_score is None
        assert result.pipeline_quality_passed is None
        assert result.errors is None

    def test_order_by_started_at(self, db_session: Session):
        """Test ordering by start time."""
        t1 = datetime(2026, 1, 1, tzinfo=timezone.utc)
        t2 = datetime(2026, 1, 2, tzinfo=timezone.utc)
        t3 = datetime(2026, 1, 3, tzinfo=timezone.utc)

        db_session.add(_make_run(run_id="r1", started_at=t2))
        db_session.add(_make_run(run_id="r2", started_at=t1))
        db_session.add(_make_run(run_id="r3", started_at=t3))
        db_session.commit()

        runs = (
            db_session.query(PipelineRunModel)
            .order_by(PipelineRunModel.started_at.desc())
            .all()
        )
        assert runs[0].run_id == "r3"
        assert runs[1].run_id == "r1"
        assert runs[2].run_id == "r2"


# ---------------------------------------------------------------------------
# TestRunsAPIModels
# ---------------------------------------------------------------------------


class TestRunsAPIModels:
    """Tests for API request/response models."""

    def test_pipeline_run_response(self):
        """Test PipelineRunResponse model."""
        from pycharter.api.models.runs import PipelineRunResponse

        resp = PipelineRunResponse(
            id="test-uuid",
            run_id="abc123",
            status="success",
            rows_loaded=100,
        )
        assert resp.run_id == "abc123"
        assert resp.rows_loaded == 100
        assert resp.pipeline_name is None

    def test_run_stats_response_defaults(self):
        """Test RunStatsResponse defaults."""
        from pycharter.api.models.runs import RunStatsResponse

        resp = RunStatsResponse()
        assert resp.total_runs == 0
        assert resp.success_rate == 0.0
        assert resp.avg_duration_seconds is None

    def test_timeseries_bucket(self):
        """Test TimeseriesBucket model."""
        from pycharter.api.models.runs import TimeseriesBucket

        bucket = TimeseriesBucket(
            timestamp="2026-01-01",
            total=10,
            successful=8,
            failed=2,
        )
        assert bucket.total == 10
        assert bucket.avg_duration is None

    def test_pipeline_summary(self):
        """Test PipelineSummary model."""
        from pycharter.api.models.runs import PipelineSummary

        summary = PipelineSummary(
            pipeline_name="orders",
            total_runs=50,
            successful_runs=48,
            failed_runs=2,
            success_rate=96.0,
        )
        assert summary.pipeline_name == "orders"
        assert summary.success_rate == 96.0

    def test_pipeline_run_list_response(self):
        """Test PipelineRunListResponse model."""
        from pycharter.api.models.runs import (
            PipelineRunListResponse,
            PipelineRunResponse,
        )

        resp = PipelineRunListResponse(
            runs=[
                PipelineRunResponse(id="1", run_id="r1", status="success"),
            ],
            total=1,
        )
        assert len(resp.runs) == 1
        assert resp.total == 1


# ---------------------------------------------------------------------------
# TestSavePipelineRun
# ---------------------------------------------------------------------------


class TestSavePipelineRun:
    """Tests for the _save_pipeline_run helper in etl.py."""

    def test_saves_successful_run(self, db_session: Session):
        """Test that a successful PipelineResult is persisted."""
        from pycharter.api.routes.v1.etl import _save_pipeline_run

        result = PipelineResult(
            success=True,
            rows_extracted=100,
            rows_transformed=100,
            rows_loaded=95,
            rows_failed=5,
            duration_seconds=2.5,
            pipeline_name="test-pipe",
            run_id="sv123456",
            start_time=datetime.now(timezone.utc),
            end_time=datetime.now(timezone.utc),
            batches_processed=1,
            batch_results=[
                BatchResult(batch_index=0, rows_in=100, rows_out=95),
            ],
        )

        _save_pipeline_run(
            db_session,
            result,
            extract_config={"type": "http", "url": "https://example.com"},
            load_config={"type": "postgres", "table": "users"},
            dry_run=False,
        )

        saved = db_session.query(PipelineRunModel).first()
        assert saved is not None
        assert saved.run_id == "sv123456"
        assert saved.status == "success"
        assert saved.rows_loaded == 95
        assert saved.extractor_type == "http"
        assert saved.loader_type == "postgres"
        assert saved.trigger == "api"
        assert saved.config_snapshot["extract"]["type"] == "http"

    def test_saves_failed_run(self, db_session: Session):
        """Test that a failed PipelineResult is persisted with status=failed."""
        from pycharter.api.routes.v1.etl import _save_pipeline_run

        result = PipelineResult(
            success=False,
            rows_extracted=100,
            rows_loaded=0,
            rows_failed=100,
            run_id="fl123456",
            errors=["Connection refused"],
        )

        _save_pipeline_run(db_session, result, {}, {}, dry_run=False)

        saved = db_session.query(PipelineRunModel).first()
        assert saved is not None
        assert saved.status == "failed"
        assert saved.errors == ["Connection refused"]

    def test_saves_partial_run(self, db_session: Session):
        """Test that a partial success is persisted with status=partial."""
        from pycharter.api.routes.v1.etl import _save_pipeline_run

        result = PipelineResult(
            success=False,
            rows_extracted=100,
            rows_loaded=50,
            rows_failed=50,
            run_id="pa123456",
        )

        _save_pipeline_run(db_session, result, {}, {}, dry_run=False)

        saved = db_session.query(PipelineRunModel).first()
        assert saved is not None
        assert saved.status == "partial"

    def test_dry_run_trigger(self, db_session: Session):
        """Test that dry_run sets trigger to 'dry_run'."""
        from pycharter.api.routes.v1.etl import _save_pipeline_run

        result = PipelineResult(
            success=True,
            rows_loaded=100,
            run_id="dr123456",
        )

        _save_pipeline_run(db_session, result, {}, {}, dry_run=True)

        saved = db_session.query(PipelineRunModel).first()
        assert saved is not None
        assert saved.trigger == "dry_run"

    def test_save_failure_does_not_propagate(self, db_session: Session):
        """Test that save failure is swallowed (logged, not raised)."""
        from pycharter.api.routes.v1.etl import _save_pipeline_run

        # Use a run_id that's already taken to trigger a commit error
        existing = _make_run(run_id="ex123456")
        db_session.add(existing)
        db_session.commit()

        result = PipelineResult(
            success=True,
            run_id="ex123456",  # duplicate
            rows_loaded=10,
        )

        # Should not raise
        _save_pipeline_run(db_session, result, {}, {}, dry_run=False)

    def test_generates_run_id_if_missing(self, db_session: Session):
        """Test that a run_id is generated if PipelineResult has none."""
        from pycharter.api.routes.v1.etl import _save_pipeline_run

        result = PipelineResult(
            success=True,
            rows_loaded=10,
            run_id=None,
        )

        _save_pipeline_run(db_session, result, {}, {}, dry_run=False)

        saved = db_session.query(PipelineRunModel).first()
        assert saved is not None
        assert len(saved.run_id) == 8

    def test_saves_pipeline_quality_passed_when_quality_report_present(
        self, db_session: Session
    ):
        """Test that pipeline_quality_passed is set from result.quality_report."""
        from pycharter.api.routes.v1.etl import _save_pipeline_run
        from pycharter.etl_generator.quality import (
            QualityCheckResult,
            QualityCheckSeverity,
            QualityCheckStatus,
            QualityReport,
        )

        report = QualityReport(
            checks=(
                QualityCheckResult(
                    check_name="row_count",
                    check_type="row_count",
                    status=QualityCheckStatus.PASSED,
                    severity=QualityCheckSeverity.FAIL,
                    message="OK",
                    details={},
                ),
            ),
            passed=True,
        )
        result = PipelineResult(
            success=True,
            rows_loaded=10,
            run_id="qp123456",
            quality_report=report,
        )
        _save_pipeline_run(
            db_session, result, {"type": "file"}, {"type": "file"}, False
        )
        saved = db_session.query(PipelineRunModel).first()
        assert saved is not None
        assert saved.pipeline_quality_passed is True
        assert saved.contract_quality_score is None
        assert saved.contract_quality_passed is None


# ---------------------------------------------------------------------------
# TestModelToResponse
# ---------------------------------------------------------------------------


class TestModelToResponse:
    """Tests for the _model_to_response helper."""

    def test_converts_model_to_response(self, db_session: Session):
        """Test that a model is correctly converted to a response."""
        from pycharter.api.routes.v1.runs import _model_to_response

        run = _make_run()
        db_session.add(run)
        db_session.commit()

        result = db_session.query(PipelineRunModel).first()
        resp = _model_to_response(result)

        assert resp.run_id == "abc12345"
        assert resp.status == "success"
        assert resp.rows_loaded == 95
        assert resp.rows_failed == 5
        assert resp.duration_seconds == 2.5
        assert resp.contract_quality_score is None
        assert resp.pipeline_quality_passed is None

    def test_pipeline_result_to_etl_response_includes_pipeline_quality_report(self):
        """Test that EtlRunResponse has pipeline_quality_report when result.quality_report is set."""
        from pycharter.api.routes.v1.etl import _pipeline_result_to_etl_response
        from pycharter.etl_generator.quality import (
            QualityCheckResult,
            QualityCheckSeverity,
            QualityCheckStatus,
            QualityReport,
        )

        report = QualityReport(
            checks=(
                QualityCheckResult(
                    check_name="row_count",
                    check_type="row_count",
                    status=QualityCheckStatus.PASSED,
                    severity=QualityCheckSeverity.FAIL,
                    message="Count OK",
                    details={"min": 1},
                ),
            ),
            passed=True,
        )
        result = PipelineResult(
            success=True,
            rows_extracted=5,
            rows_loaded=5,
            run_id="er123456",
            quality_report=report,
        )
        resp = _pipeline_result_to_etl_response(result)
        assert resp.run_id == "er123456"
        assert resp.pipeline_quality_report is not None
        assert resp.pipeline_quality_report.passed is True
        assert len(resp.pipeline_quality_report.checks) == 1
        assert resp.pipeline_quality_report.checks[0].check_type == "row_count"

    def test_pipeline_result_to_etl_response_no_quality_report(self):
        """Test that EtlRunResponse has pipeline_quality_report=None when no quality_report."""
        from pycharter.api.routes.v1.etl import _pipeline_result_to_etl_response

        result = PipelineResult(
            success=True,
            rows_loaded=10,
            run_id="nr123456",
        )
        resp = _pipeline_result_to_etl_response(result)
        assert resp.pipeline_quality_report is None
