"""
Azure Data Factory Tests

Unit tests run by default (mocked HTTP via responses library).
Integration tests require real credentials and are marked with @pytest.mark.integration.

To run all tests:
    pytest tests/test_datafactory.py -m ""

To run only integration tests:
    pytest tests/test_datafactory.py -m integration
"""

import os

import pytest
import responses
from dotenv import load_dotenv
from responses import matchers

from fabias._shared.auth import ServicePrincipalAuth
from fabias._shared.exceptions import FabiasError
from fabias.integrations import adf

# =============================================================================
# FIXTURES
# =============================================================================


@pytest.fixture
def mock_auth():
    """Mock ServicePrincipalAuth for unit tests"""
    return ServicePrincipalAuth(
        tenant_id="fake-tenant-id", client_id="fake-client-id", client_secret="fake-client-secret"
    )


@pytest.fixture
def mock_client(mock_auth):
    """Create a DataFactoryClient with mocked auth"""
    return adf.DataFactoryClient(
        subscription_id="test-sub-123",
        resource_group="test-rg",
        factory="test-factory",
        auth=mock_auth,
    )


@pytest.fixture
def sample_pipeline_data():
    """Sample pipeline data matching Azure API response structure"""
    return {
        "id": "/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline",
        "name": "TestPipeline",
        "properties": {
            "activities": [
                {"name": "Activity1", "type": "Copy"},
                {"name": "Activity2", "type": "ExecuteDataFlow"},
            ],
            "parameters": {
                "InputPath": {"type": "String", "defaultValue": "/input"},
                "OutputPath": {"type": "String", "defaultValue": "/output"},
            },
            "variables": {},
            "lastPublishTime": "2025-01-15T10:00:00Z",
        },
    }


# =============================================================================
# UNIT TESTS - Pipeline Run Success
# =============================================================================


class TestPipelineRunSuccess:
    """Test successful pipeline run scenarios"""

    @responses.activate
    def test_pipeline_run_returns_job(self, mock_auth):
        """Test pipeline.run() returns a Job with runId"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline/createRun",
            json={"runId": "test-run-123"},
            status=202,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")
        job = pipeline.run()

        assert job.job.id == "test-run-123"
        assert job.job.status == "initialized"  # Not checked yet

    @responses.activate
    def test_pipeline_run_with_200_status(self, mock_auth):
        """Test pipeline.run() when ADF returns 200 instead of 202 (real-world behavior)"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun - returns 200 (not 202) but still long-running
        # This is the actual behavior seen in production ADF
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline/createRun",
            json={"runId": "test-run-456"},
            status=200,  # Real ADF returns 200, not 202!
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")
        job = pipeline.run()

        # Should still be treated as long-running operation
        assert job.longRunning is True
        assert job.job.id == "test-run-456"
        assert job.job.status == "initialized"  # Not yet checked via API
        assert job.job.location is not None  # Should build status URL

    @responses.activate
    def test_pipeline_run_with_parameters(self, mock_auth):
        """Test pipeline.run() with parameters"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun with parameter validation
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline/createRun",
            json={"runId": "test-run-with-params"},
            status=202,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")
        job = pipeline.run({"InputPath": "/custom/path"})

        assert job.job.id == "test-run-with-params"

    @responses.activate
    def test_job_check_status(self, mock_auth):
        """Test Job.check_status() updates job state"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline/createRun",
            json={"runId": "test-run-456"},
            status=202,
        )

        # Mock status check
        responses.add(
            responses.GET,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelineruns/test-run-456",
            json={
                "runId": "test-run-456",
                "status": "InProgress",
                "message": "Running activity 1 of 2",
            },
            status=200,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")
        resp = pipeline.run()

        resp.checkStatus()
        assert resp.job.status == "InProgress"
        assert resp.job.message == "Running activity 1 of 2"

    @responses.activate
    def test_job_wait_success(self, mock_auth):
        """Test Job.wait() polls until completion"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline/createRun",
            json={"runId": "run-wait-success"},
            status=202,
        )

        # Mock status checks - InProgress then Succeeded
        responses.add(
            responses.GET,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelineruns/run-wait-success",
            json={"runId": "run-wait-success", "status": "InProgress"},
            status=200,
        )

        responses.add(
            responses.GET,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelineruns/run-wait-success",
            json={"runId": "run-wait-success", "status": "Succeeded"},
            status=200,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")
        resp = pipeline.run()

        # Wait with callback
        callback_statuses = []

        def status_callback(j):
            callback_statuses.append(j.job.status)

        # Override retry interval for fast test
        resp.job.retry = 0.01
        resp.wait(callback=status_callback, timeout=5)

        assert resp.job.status == "Succeeded"
        assert len(callback_statuses) >= 1


# =============================================================================
# UNIT TESTS - Pipeline Run Failures
# =============================================================================


class TestPipelineRunFailures:
    """Test pipeline run failure scenarios"""

    @responses.activate
    def test_pipeline_run_fails_no_runid(self, mock_auth):
        """Test pipeline.run() raises error when no runId returned"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun - returns success but no runId
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline/createRun",
            json={"message": "Created but no runId"},
            status=200,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")

        with pytest.raises(FabiasError, match="No runId returned"):
            _ = pipeline.run()

    @responses.activate
    def test_pipeline_run_fails_http_error(self, mock_auth):
        """Test pipeline.run() raises error on HTTP failure"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun - returns 400 error
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/BadPipeline/createRun",
            json={"error": {"message": "Invalid pipeline parameters"}},
            status=400,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("BadPipeline")

        with pytest.raises(Exception, match="Invalid pipeline parameters"):
            _ = pipeline.run({"bad": "params"})

    @responses.activate
    def test_pipeline_run_fails_404(self, mock_auth):
        """Test pipeline.run() fails when pipeline doesn't exist"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun - pipeline not found
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/NonExistent/createRun",
            json={"error": {"message": "Pipeline not found"}},
            status=404,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("NonExistent")

        with pytest.raises(Exception, match="Pipeline not found"):
            _ = pipeline.run()

    @responses.activate
    def test_pipeline_run_fails_unexpected_status_code(self, mock_auth):
        """Test pipeline.run() fails on unexpected success status (not 200/201/202)"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun - returns 203 which is success but not in expected list
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline/createRun",
            json={"message": "Accepted but unusual status"},
            status=203,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")

        with pytest.raises(FabiasError, match="Unexpected status code 203"):
            _ = pipeline.run()

    @responses.activate
    def test_job_status_failed(self, mock_auth):
        """Test Job.check_status() when pipeline run fails"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline/createRun",
            json={"runId": "run-fail-123"},
            status=202,
        )

        # Mock status check - failed
        responses.add(
            responses.GET,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelineruns/run-fail-123",
            json={
                "runId": "run-fail-123",
                "status": "Failed",
                "message": "Pipeline activity failed",
            },
            status=200,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")
        job = pipeline.run()

        job.checkStatus()
        assert job.job.status == "Failed"
        assert job.job.message == "Pipeline activity failed"

    @responses.activate
    def test_job_wait_fails(self, mock_auth):
        """Test Job.wait() when pipeline run fails"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock createRun
        responses.add(
            responses.POST,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline/createRun",
            json={"runId": "run-will-fail"},
            status=202,
        )

        # Mock status checks - InProgress then Failed
        responses.add(
            responses.GET,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelineruns/run-will-fail",
            json={"runId": "run-will-fail", "status": "InProgress"},
            status=200,
        )

        responses.add(
            responses.GET,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelineruns/run-will-fail",
            json={
                "runId": "run-will-fail",
                "status": "Failed",
                "message": "Activity execution error",
            },
            status=200,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")
        job = pipeline.run()

        # Wait should raise OperationError on failure
        callback_statuses = []

        def status_callback(j):
            callback_statuses.append(j.job.status)

        # Override retry interval for fast test
        job.job.retry = 0.01

        from fabias._shared.exceptions import OperationError

        with pytest.raises(OperationError, match="Operation failed"):
            job.wait(callback=status_callback, timeout=5, raiseFailure=True)


# =============================================================================
# UNIT TESTS - Pipeline Lazy Loading
# =============================================================================


class TestPipelineLazyLoading:
    """Test pipeline lazy loading and caching behavior"""

    @responses.activate
    def test_pipeline_lazy_fetch_on_property_access(self, mock_auth, sample_pipeline_data):
        """Test pipeline data is fetched only when property is accessed"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # Mock GET pipeline
        responses.add(
            responses.GET,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline",
            json=sample_pipeline_data,
            status=200,
            match=[matchers.query_param_matcher({"api-version": "2018-06-01"})],
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")

        # No API call yet
        assert pipeline.name == "TestPipeline"
        assert len(responses.calls) == 0

        # First property access triggers fetch
        activities = pipeline.activities
        assert len(activities) == 2
        assert len(responses.calls) == 2  # auth + get pipeline

        # Subsequent property accesses use cache
        _ = pipeline.parameters
        _ = pipeline.lastPublished
        assert len(responses.calls) == 2  # No new calls

    @responses.activate
    def test_pipeline_refresh_fetches_new_data(self, mock_auth, sample_pipeline_data):
        """Test pipeline.refresh() fetches updated data"""
        # Mock auth token
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token",
            json={"access_token": "fake-token", "expires_in": 3600},
            status=200,
        )

        # First fetch
        responses.add(
            responses.GET,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline",
            json=sample_pipeline_data,
            status=200,
        )

        # Second fetch after refresh
        updated_data = sample_pipeline_data.copy()
        updated_data["properties"]["lastPublishTime"] = "2025-01-20T15:00:00Z"
        responses.add(
            responses.GET,
            "https://management.azure.com/subscriptions/test-sub-123/resourceGroups/test-rg/providers/Microsoft.DataFactory/factories/test-factory/pipelines/TestPipeline",
            json=updated_data,
            status=200,
        )

        client = adf.DataFactoryClient(
            subscription_id="test-sub-123",
            resource_group="test-rg",
            factory="test-factory",
            auth=mock_auth,
        )

        pipeline = client.pipeline("TestPipeline")
        assert pipeline.lastPublished == "2025-01-15T10:00:00Z"

        pipeline.refresh()
        assert pipeline.lastPublished == "2025-01-20T15:00:00Z"


# =============================================================================
# INTEGRATION TESTS - Real API calls (require credentials)
# =============================================================================

# Global auth for integration tests (set in test_authentication)
_auth = None


@pytest.mark.integration
class TestIntegration:
    """Integration tests that call the real Azure Data Factory API.

    Requires .env file with:
        AZURE_TENANT_ID
        AZURE_CLIENT_ID
        AZURE_CLIENT_SECRET
        SUBSCRIPTION_ID
        RESOURCE_GROUP
        FACTORY_NAME
    """

    def test_authentication(self):
        """Test service principal authentication"""
        global _auth
        load_dotenv()

        _auth = ServicePrincipalAuth(
            tenant_id=os.environ["AZURE_TENANT_ID"],
            client_id=os.environ["AZURE_CLIENT_ID"],
            client_secret=os.environ["AZURE_CLIENT_SECRET"],
        )

        assert _auth is not None
        # Token should be fetchable (lazy, but proves creds are valid format)

    def test_list_pipelines(self):
        """Test listing pipelines from real factory"""
        load_dotenv()

        client = adf.DataFactoryClient(
            subscription_id=os.environ["SUBSCRIPTION_ID"],
            resource_group=os.environ["RESOURCE_GROUP"],
            factory=os.environ["FACTORY_NAME"],
            auth=_auth,
        )

        pipelines = client.list_pipelines()
        assert isinstance(pipelines, list)
        # At least one pipeline should exist in test factory
        assert len(pipelines) > 0

    def test_fetch_pipeline(self):
        """Test fetching a specific pipeline"""
        load_dotenv()

        client = adf.DataFactoryClient(
            subscription_id=os.environ["SUBSCRIPTION_ID"],
            resource_group=os.environ["RESOURCE_GROUP"],
            factory=os.environ["FACTORY_NAME"],
            auth=_auth,
        )

        # Get first pipeline from list
        pipelines = client.list_pipelines()
        assert len(pipelines) > 0

        pipeline_name = pipelines[0]["name"]
        pipeline = client.pipeline(pipeline_name)

        assert pipeline.name == pipeline_name
        # Activities should be fetchable
        assert hasattr(pipeline, "activities")

    def test_failed_authentication(self):
        """Test that bad credentials fail appropriately"""
        bad_auth = ServicePrincipalAuth(
            tenant_id="00000000-0000-0000-0000-000000000000",
            client_id="bad-client-id",
            client_secret="bad-secret",
        )

        with pytest.raises(Exception):
            # Should fail when trying to get token
            bad_auth.get_token()
