"""
Tests for OpenLineage integration (lineage.py).

All tests mock the openlineage-python package so they run without it installed.
"""

from __future__ import annotations

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

import pytest

from pycharter.etl_generator.lineage import (
    _sanitize_url,
    create_lineage_emitter,
    dataset_from_extract_config,
    dataset_from_load_config,
)
from pycharter.etl_generator.result import PipelineResult

# =========================================================================
# Dataset mappers
# =========================================================================


class TestDatasetFromExtractConfig:
    def test_http(self):
        ns, name = dataset_from_extract_config(
            {
                "type": "http",
                "base_url": "https://api.example.com",
                "api_endpoint": "/v2/prices",
            }
        )
        assert ns == "https://api.example.com"
        assert name == "/v2/prices"

    def test_http_url_fallback(self):
        ns, name = dataset_from_extract_config(
            {"type": "http", "url": "https://api.example.com/data"}
        )
        assert ns == "https://api.example.com/data"
        assert name == "/"

    def test_file(self):
        ns, name = dataset_from_extract_config(
            {"type": "file", "path": "/data/input.csv"}
        )
        assert ns == "file"
        assert name == "/data/input.csv"

    def test_database(self):
        ns, name = dataset_from_extract_config(
            {
                "type": "database",
                "database": {"url": "postgresql://user:pass@host/db"},
                "query": "SELECT * FROM users",
            }
        )
        assert "***" in ns
        assert "pass" not in ns
        assert name == "SELECT * FROM users"

    def test_cloud_storage(self):
        ns, name = dataset_from_extract_config(
            {
                "type": "cloud_storage",
                "storage": {"provider": "s3", "bucket": "my-bucket", "path": "data/"},
            }
        )
        assert ns == "s3://my-bucket"
        assert name == "data/"

    def test_mongodb(self):
        ns, name = dataset_from_extract_config(
            {
                "type": "mongodb",
                "mongodb": {
                    "connection_string": "mongodb://user:pw@host:27017/db",
                    "collection": "users",
                },
            }
        )
        assert "***" in ns
        assert "pw" not in ns
        assert name == "users"

    def test_unknown_type(self):
        ns, name = dataset_from_extract_config({"type": "custom_source"})
        assert ns == "unknown"
        assert name == "custom_source"


class TestDatasetFromLoadConfig:
    def test_postgres(self):
        ns, name = dataset_from_load_config(
            {"type": "postgres", "table": "stock_prices", "schema_name": "scraper"}
        )
        assert ns == "postgres"
        assert name == "scraper.stock_prices"

    def test_postgres_with_database_url(self):
        ns, name = dataset_from_load_config(
            {
                "type": "postgres",
                "table": "orders",
                "database": {"url": "postgresql://u:p@host/mydb"},
            }
        )
        assert "***" in ns
        assert "p" not in ns or "***" in ns
        assert name == "public.orders"

    def test_file(self):
        ns, name = dataset_from_load_config(
            {"type": "file", "path": "/output/result.json"}
        )
        assert ns == "file"
        assert name == "/output/result.json"

    def test_cloud_storage(self):
        ns, name = dataset_from_load_config(
            {
                "type": "cloud_storage",
                "storage": {"provider": "gcs", "bucket": "out-bucket", "path": "out/"},
            }
        )
        assert ns == "gcs://out-bucket"
        assert name == "out/"

    def test_unknown_type(self):
        ns, name = dataset_from_load_config({"type": "something_else"})
        assert ns == "unknown"


# =========================================================================
# Sanitize URL
# =========================================================================


class TestSanitizeUrl:
    def test_strips_credentials(self):
        assert (
            _sanitize_url("postgresql://user:pass@host/db")
            == "postgresql://***@host/db"
        )

    def test_no_credentials(self):
        assert _sanitize_url("file:///data/out") == "file:///data/out"


# =========================================================================
# create_lineage_emitter
# =========================================================================


class TestCreateLineageEmitter:
    def test_returns_none_when_no_lineage_key(self):
        assert create_lineage_emitter({}) is None

    def test_returns_none_when_disabled(self):
        assert create_lineage_emitter({"lineage": {"enabled": False}}) is None

    def test_returns_none_when_openlineage_not_installed(self):
        with patch.dict(
            "sys.modules", {"openlineage": None, "openlineage.client": None}
        ):
            result = create_lineage_emitter(
                {"lineage": {"enabled": True, "namespace": "test"}}
            )
            assert result is None

    def test_returns_emitter_when_enabled(self):
        mock_client_cls = MagicMock()
        with patch(
            "pycharter.etl_generator.lineage.LineageEmitter.__init__",
            return_value=None,
        ) as mock_init:
            emitter = create_lineage_emitter(
                {"lineage": {"enabled": True, "namespace": "test"}}
            )
            assert emitter is not None


# =========================================================================
# Facet builders (mocked openlineage imports)
# =========================================================================


class TestFacetBuilders:
    def _make_result(self, success=True, errors=None):
        return PipelineResult(
            success=success,
            rows_extracted=500,
            rows_transformed=490,
            rows_loaded=480,
            rows_failed=10,
            rows_quarantined_extract=5,
            rows_quarantined_load=5,
            batches_processed=3,
            duration_seconds=12.5,
            validation_errors_extract=["err1"],
            validation_errors_load=[],
            errors=errors or [],
        )

    @pytest.fixture(autouse=True)
    def _skip_if_no_openlineage(self):
        """Skip facet builder tests if openlineage-python is not installed."""
        pytest.importorskip("openlineage")

    def test_build_run_facets_success(self):
        from pycharter.etl_generator.lineage import build_run_facets

        result = self._make_result()
        facets = build_run_facets(result)

        assert "pycharter_pipeline" in facets
        assert "errorMessage" not in facets
        stats = facets["pycharter_pipeline"]
        assert stats["rows_extracted"] == 500
        assert stats["rows_loaded"] == 480
        assert stats["duration_seconds"] == 12.5
        assert stats["batches_processed"] == 3
        assert stats["total_quarantined"] == 10

    def test_build_run_facets_failure(self):
        from pycharter.etl_generator.lineage import build_run_facets

        result = self._make_result(
            success=False, errors=["Connection refused", "Timeout"]
        )
        facets = build_run_facets(result)

        assert "errorMessage" in facets
        err = facets["errorMessage"]
        assert err.message == "Connection refused"
        assert "pycharter_pipeline" in facets

    def test_build_input_facets(self):
        from pycharter.etl_generator.lineage import build_input_facets

        result = self._make_result()
        facets = build_input_facets(result)

        assert "dataQualityMetrics" in facets
        assert facets["dataQualityMetrics"].rowCount == 500

    def test_build_output_facets(self):
        from pycharter.etl_generator.lineage import build_output_facets

        result = self._make_result()
        facets = build_output_facets(result)

        assert "outputStatistics" in facets
        assert facets["outputStatistics"].rowCount == 480


# =========================================================================
# LineageEmitter (mocked client)
# =========================================================================


class TestLineageEmitter:
    @pytest.fixture(autouse=True)
    def _skip_if_no_openlineage(self):
        pytest.importorskip("openlineage")

    def _make_emitter(self):
        from pycharter.etl_generator.lineage import LineageEmitter

        # Build emitter without calling __init__ (avoids needing OpenLineageClient).
        emitter = LineageEmitter.__new__(LineageEmitter)
        emitter._client = MagicMock()
        emitter._namespace = "test-ns"
        return emitter

    def test_start_emits_event(self):
        emitter = self._make_emitter()
        emitter.start(
            "my_job",
            "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            [("http://api", "/users")],
        )
        emitter._client.emit.assert_called_once()
        event = emitter._client.emit.call_args[0][0]
        assert event.eventType.name == "START"

    def test_complete_emits_event_with_facets(self):
        emitter = self._make_emitter()
        result = PipelineResult(
            success=True, rows_extracted=100, rows_loaded=95, duration_seconds=5.0
        )
        emitter.complete(
            "my_job",
            "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            [("http://api", "/users")],
            [("postgres", "public.users")],
            result,
        )
        emitter._client.emit.assert_called_once()
        event = emitter._client.emit.call_args[0][0]
        assert event.eventType.name == "COMPLETE"
        assert event.run.facets.get("pycharter_pipeline") is not None

    def test_fail_emits_event_with_error_facet(self):
        emitter = self._make_emitter()
        result = PipelineResult(
            success=False,
            rows_extracted=50,
            errors=["Connection timeout"],
        )
        emitter.fail(
            "my_job",
            "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            [("http://api", "/users")],
            result,
        )
        emitter._client.emit.assert_called_once()
        event = emitter._client.emit.call_args[0][0]
        assert event.eventType.name == "FAIL"
        assert "errorMessage" in event.run.facets


# =========================================================================
# Error isolation: lineage failures must not break pipeline
# =========================================================================


class TestLineageErrorIsolation:
    def test_create_emitter_swallows_unexpected_error(self):
        with patch(
            "pycharter.etl_generator.lineage.LineageEmitter.__init__",
            side_effect=RuntimeError("boom"),
        ):
            result = create_lineage_emitter(
                {"lineage": {"enabled": True, "namespace": "test"}}
            )
            assert result is None
