"""Tests for the Supabase Neptune proxy.

These tests verify that the Supabase Edge Function correctly proxies
requests to the Neptune.ai API.

Environment variables required:
  - NEPTUNE_API_TOKEN: Your base64-encoded Neptune API token
  - NEPTUNE_PROJECT: Your Neptune project identifier (format: workspace/project)

Proxy mode (set NEPTUNE_PROXY global variable):
  - 'local': Use local Supabase (localhost:54321)
  - 'remote': Use remote Supabase (hardcoded URL)

Example usage:
  # Local Supabase (after running `supabase start`)
  export NEPTUNE_API_TOKEN="eyJ..."
  export NEPTUNE_PROJECT="strakam/tracking"
  pytest goodseed/tests/test_neptune_proxy.py -v

  # Remote Supabase (change NEPTUNE_PROXY in the file to "remote")
  export NEPTUNE_API_TOKEN="eyJ..."
  export NEPTUNE_PROJECT="strakam/tracking"
  pytest goodseed/tests/test_neptune_proxy.py -v
"""

import base64
import json
import os
from typing import Any, Dict, Literal, Optional

import httpx
import pytest


# =============================================================================
# Proxy Mode Configuration
# =============================================================================

NeptuneProxyMode = Literal["local", "remote"]

LOCAL_SUPABASE_URL = "http://localhost:54321"
REMOTE_SUPABASE_URL = "https://llyffzohdrzxomnblpjx.supabase.co"

# Global configuration - change this to switch proxy modes
NEPTUNE_PROXY: NeptuneProxyMode = "remote"


def set_neptune_proxy(mode: NeptuneProxyMode) -> None:
    """Set the Neptune proxy mode ('local' or 'remote')."""
    global NEPTUNE_PROXY
    NEPTUNE_PROXY = mode
    print(f"[Neptune] Proxy mode set to: {mode}")


def get_neptune_proxy() -> NeptuneProxyMode:
    """Get the current Neptune proxy mode."""
    return NEPTUNE_PROXY


# =============================================================================
# Configuration
# =============================================================================

def get_supabase_url() -> str:
    """Get Supabase URL based on proxy mode."""
    if NEPTUNE_PROXY == "remote":
        return REMOTE_SUPABASE_URL
    # Local mode
    return LOCAL_SUPABASE_URL


def get_neptune_token() -> str:
    """Get Neptune API token from environment."""
    token = os.environ.get("NEPTUNE_API_TOKEN")
    if not token:
        pytest.skip("NEPTUNE_API_TOKEN environment variable not set")
    return token


def get_neptune_project() -> str:
    """Get Neptune project from environment."""
    project = os.environ.get("NEPTUNE_PROJECT")
    if not project:
        pytest.skip("NEPTUNE_PROJECT environment variable not set")
    return project


def parse_neptune_token(token: str) -> Dict[str, str]:
    """Parse base64-encoded Neptune API token."""
    try:
        decoded = base64.b64decode(token).decode("utf-8")
        data = json.loads(decoded)
        return {
            "api_address": data.get("api_address") or data.get("api_url"),
            "api_key": data.get("api_key"),
        }
    except Exception as e:
        pytest.fail(f"Failed to parse Neptune token: {e}")


# =============================================================================
# Test Fixtures
# =============================================================================

@pytest.fixture(scope="module")
def proxy_base_url() -> str:
    """Get the Neptune proxy base URL."""
    supabase_url = get_supabase_url()
    return f"{supabase_url}/functions/v1/neptune-proxy"


@pytest.fixture(scope="module")
def neptune_token() -> str:
    """Get Neptune API token."""
    return get_neptune_token()


@pytest.fixture(scope="module")
def neptune_project() -> str:
    """Get Neptune project identifier."""
    return get_neptune_project()


@pytest.fixture(scope="module")
def http_client() -> httpx.Client:
    """Create an HTTP client for testing."""
    client = httpx.Client(timeout=30.0)
    yield client
    client.close()


@pytest.fixture(scope="module")
def access_token(proxy_base_url: str, neptune_token: str, http_client: httpx.Client) -> str:
    """Exchange Neptune API token for access token through the proxy."""
    response = http_client.get(
        f"{proxy_base_url}/api/backend/v1/authorization/oauth-token",
        headers={"X-Neptune-Api-Token": neptune_token},
    )
    
    if response.status_code != 200:
        pytest.fail(
            f"Failed to exchange token: {response.status_code} - {response.text[:500]}"
        )
    
    data = response.json()
    if "accessToken" not in data:
        pytest.fail(f"No accessToken in response: {data}")
    
    return data["accessToken"]


# =============================================================================
# Test: Proxy Health / Basic Connectivity
# =============================================================================

class TestProxyConnectivity:
    """Tests for basic proxy connectivity."""

    def test_cors_preflight(self, proxy_base_url: str, http_client: httpx.Client):
        """Test that CORS preflight requests are handled correctly."""
        response = http_client.options(
            f"{proxy_base_url}/api/backend/v1/authorization/oauth-token",
            headers={
                "Origin": "http://localhost:5173",
                "Access-Control-Request-Method": "GET",
                "Access-Control-Request-Headers": "X-Neptune-Api-Token",
            },
        )
        
        assert response.status_code == 200, f"CORS preflight failed: {response.status_code}"
        
        # Check CORS headers - at least one of the CORS response headers should be present
        cors_headers = [
            "access-control-allow-origin",
            "access-control-allow-methods",
            "access-control-allow-headers",
        ]
        has_cors = any(h in response.headers for h in cors_headers)
        assert has_cors, f"No CORS headers in response: {dict(response.headers)}"

    def test_proxy_forwards_requests(
        self, proxy_base_url: str, neptune_token: str, http_client: httpx.Client
    ):
        """Test that the proxy successfully forwards requests to Neptune."""
        # Use a known valid endpoint to verify the proxy is forwarding correctly
        response = http_client.get(
            f"{proxy_base_url}/api/backend/v1/authorization/oauth-token",
            headers={"X-Neptune-Api-Token": neptune_token},
        )
        
        # Should succeed - this verifies the proxy is working
        assert response.status_code == 200, (
            f"Proxy failed to forward request: {response.status_code} - {response.text[:200]}"
        )
        
        # Verify we got a valid JSON response with the expected structure
        data = response.json()
        assert "accessToken" in data, f"Missing accessToken in response: {data}"


# =============================================================================
# Test: Authentication Flow
# =============================================================================

class TestAuthentication:
    """Tests for Neptune authentication through the proxy."""

    def test_token_exchange_success(
        self, proxy_base_url: str, neptune_token: str, http_client: httpx.Client
    ):
        """Test that token exchange works through the proxy."""
        response = http_client.get(
            f"{proxy_base_url}/api/backend/v1/authorization/oauth-token",
            headers={"X-Neptune-Api-Token": neptune_token},
        )
        
        assert response.status_code == 200, (
            f"Token exchange failed: {response.status_code} - {response.text[:500]}"
        )
        
        data = response.json()
        assert "accessToken" in data, f"No accessToken in response: {data}"
        assert isinstance(data["accessToken"], str)
        assert len(data["accessToken"]) > 0

    def test_token_exchange_without_token_fails(
        self, proxy_base_url: str, http_client: httpx.Client
    ):
        """Test that token exchange without a token fails."""
        response = http_client.get(
            f"{proxy_base_url}/api/backend/v1/authorization/oauth-token",
        )
        
        # Should fail with 401 or similar
        assert response.status_code >= 400, (
            f"Expected error without token, got {response.status_code}"
        )

    def test_token_exchange_with_invalid_token_fails(
        self, proxy_base_url: str, http_client: httpx.Client
    ):
        """Test that token exchange with invalid token fails."""
        response = http_client.get(
            f"{proxy_base_url}/api/backend/v1/authorization/oauth-token",
            headers={"X-Neptune-Api-Token": "invalid-token-here"},
        )
        
        # Should fail with 401 or similar
        assert response.status_code >= 400, (
            f"Expected error with invalid token, got {response.status_code}"
        )


# =============================================================================
# Test: Project Info (Backend API)
# =============================================================================

class TestProjectInfo:
    """Tests for fetching project info through the proxy."""

    def test_get_project_info(
        self,
        proxy_base_url: str,
        access_token: str,
        neptune_project: str,
        http_client: httpx.Client,
    ):
        """Test fetching project info through the proxy."""
        response = http_client.get(
            f"{proxy_base_url}/api/backend/v1/projects/get",
            params={"projectIdentifier": neptune_project},
            headers={"Authorization": f"Bearer {access_token}"},
        )
        
        assert response.status_code == 200, (
            f"Failed to get project info: {response.status_code} - {response.text[:500]}"
        )
        
        data = response.json()
        
        # Verify expected fields
        assert "id" in data, f"Missing 'id' in project info: {data}"
        assert "name" in data, f"Missing 'name' in project info: {data}"
        assert "organizationId" in data, f"Missing 'organizationId' in project info: {data}"
        
        # Project name should match the part after /
        expected_name = neptune_project.split("/")[-1]
        assert data["name"] == expected_name, (
            f"Project name mismatch: expected '{expected_name}', got '{data['name']}'"
        )

    def test_get_project_info_invalid_project(
        self,
        proxy_base_url: str,
        access_token: str,
        http_client: httpx.Client,
    ):
        """Test that fetching non-existent project returns error."""
        response = http_client.get(
            f"{proxy_base_url}/api/backend/v1/projects/get",
            params={"projectIdentifier": "nonexistent/project-that-does-not-exist"},
            headers={"Authorization": f"Bearer {access_token}"},
        )
        
        # Should fail with 404 or similar
        assert response.status_code >= 400, (
            f"Expected error for non-existent project, got {response.status_code}"
        )


# =============================================================================
# Test: Run Listing (Leaderboard API)
# =============================================================================

class TestRunListing:
    """Tests for listing runs through the proxy."""

    def test_list_runs(
        self,
        proxy_base_url: str,
        access_token: str,
        neptune_project: str,
        http_client: httpx.Client,
    ):
        """Test listing runs through the proxy."""
        response = http_client.post(
            f"{proxy_base_url}/api/leaderboard/v1/leaderboard/entries/search",
            params={"projectIdentifier": neptune_project, "type": "run"},
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
            },
            json={"pagination": {"limit": 10, "offset": 0}},
        )
        
        assert response.status_code == 200, (
            f"Failed to list runs: {response.status_code} - {response.text[:500]}"
        )
        
        data = response.json()
        
        # Verify response structure
        assert "entries" in data, f"Missing 'entries' in response: {data}"
        assert isinstance(data["entries"], list), f"'entries' should be a list: {type(data['entries'])}"
        
        # If there are runs, verify their structure
        if len(data["entries"]) > 0:
            run = data["entries"][0]
            assert "experimentId" in run, f"Missing 'experimentId' in run: {run}"
            assert "attributes" in run, f"Missing 'attributes' in run: {run}"

    def test_list_runs_with_query(
        self,
        proxy_base_url: str,
        access_token: str,
        neptune_project: str,
        http_client: httpx.Client,
    ):
        """Test listing runs with NQL query through the proxy."""
        response = http_client.post(
            f"{proxy_base_url}/api/leaderboard/v1/leaderboard/entries/search",
            params={"projectIdentifier": neptune_project, "type": "run"},
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
            },
            json={
                "pagination": {"limit": 5, "offset": 0},
                # Simple NQL query - this might match nothing, but should not error
                "query": {"query": '`sys/state`:experimentState = "active"'},
            },
        )
        
        assert response.status_code == 200, (
            f"Failed to query runs: {response.status_code} - {response.text[:500]}"
        )
        
        data = response.json()
        assert "entries" in data, f"Missing 'entries' in response: {data}"


# =============================================================================
# Test: Float Series (Leaderboard API)
# =============================================================================

class TestFloatSeries:
    """Tests for float series endpoints through the proxy."""

    @pytest.fixture(scope="class")
    def sample_run(
        self,
        proxy_base_url: str,
        access_token: str,
        neptune_project: str,
        http_client: httpx.Client,
    ) -> Optional[Dict[str, Any]]:
        """Get a sample run with float series for testing."""
        response = http_client.post(
            f"{proxy_base_url}/api/leaderboard/v1/leaderboard/entries/search",
            params={"projectIdentifier": neptune_project, "type": "run"},
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
            },
            json={"pagination": {"limit": 50, "offset": 0}},
        )
        
        if response.status_code != 200:
            return None
        
        data = response.json()
        entries = data.get("entries", [])
        
        # Find a run with float series
        for entry in entries:
            attrs = entry.get("attributes", [])
            for attr in attrs:
                if "floatSeriesProperties" in attr:
                    return entry
        
        return None

    def test_fetch_float_series_points(
        self,
        proxy_base_url: str,
        access_token: str,
        sample_run: Optional[Dict[str, Any]],
        http_client: httpx.Client,
    ):
        """Test fetching float series points through the proxy."""
        if sample_run is None:
            pytest.skip("No runs with float series found")
        
        experiment_id = sample_run["experimentId"]
        
        # Find a float series attribute
        float_series_attr = None
        for attr in sample_run.get("attributes", []):
            if "floatSeriesProperties" in attr:
                float_series_attr = attr["name"]
                break
        
        if float_series_attr is None:
            pytest.skip("No float series attribute found in sample run")
        
        response = http_client.get(
            f"{proxy_base_url}/api/leaderboard/v1/attributes/series/float",
            params={
                "attribute": float_series_attr,
                "experimentId": experiment_id,
                "holderType": "experiment",
                "limit": "100",
                "offset": "0",
            },
            headers={"Authorization": f"Bearer {access_token}"},
        )
        
        assert response.status_code == 200, (
            f"Failed to fetch float series: {response.status_code} - {response.text[:500]}"
        )
        
        data = response.json()
        
        # The response should contain points in one of several possible formats
        points = None
        for key in ("values", "entries", "items", "points", "series"):
            if key in data and isinstance(data[key], list):
                points = data[key]
                break
        
        assert points is not None, f"No points found in response: {data}"
        
        # Verify point structure if we have points
        if len(points) > 0:
            point = points[0]
            assert "step" in point, f"Missing 'step' in point: {point}"
            assert "value" in point, f"Missing 'value' in point: {point}"


# =============================================================================
# Test: Batch Export (Leaderboard API)
# =============================================================================

class TestBatchExport:
    """Tests for batch export endpoints through the proxy."""

    def test_batch_floatseries_csv_request(
        self,
        proxy_base_url: str,
        access_token: str,
        neptune_project: str,
        http_client: httpx.Client,
    ):
        """Test batch float series CSV export preparation through the proxy."""
        # First, get a run to use for the test
        list_response = http_client.post(
            f"{proxy_base_url}/api/leaderboard/v1/leaderboard/entries/search",
            params={"projectIdentifier": neptune_project, "type": "run"},
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
            },
            json={"pagination": {"limit": 10, "offset": 0}},
        )
        
        if list_response.status_code != 200:
            pytest.skip("Could not list runs for batch export test")
        
        entries = list_response.json().get("entries", [])
        if len(entries) == 0:
            pytest.skip("No runs available for batch export test")
        
        # Find a run with float series
        experiment_id = None
        float_series_attr = None
        
        for entry in entries:
            for attr in entry.get("attributes", []):
                if "floatSeriesProperties" in attr:
                    experiment_id = entry["experimentId"]
                    float_series_attr = attr["name"]
                    break
            if experiment_id:
                break
        
        if not experiment_id or not float_series_attr:
            pytest.skip("No runs with float series found for batch export")
        
        # Test batch export request
        response = http_client.post(
            f"{proxy_base_url}/api/leaderboard/v1/attributes/floatSeries/csv/multiple",
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
            },
            json={
                "requests": [
                    {
                        "holderType": "experiment",
                        "holderIdentifier": experiment_id,
                        "attributes": [float_series_attr],
                    }
                ]
            },
        )
        
        assert response.status_code == 200, (
            f"Batch export request failed: {response.status_code} - {response.text[:500]}"
        )
        
        data = response.json()
        
        # Response should contain a download URL
        assert "downloadUrl" in data, f"Missing 'downloadUrl' in response: {data}"
        assert isinstance(data["downloadUrl"], str)
        assert len(data["downloadUrl"]) > 0


# =============================================================================
# Test: Fetch Endpoint (for download URLs)
# =============================================================================

class TestFetchEndpoint:
    """Tests for the _fetch endpoint used to download files."""

    def test_fetch_endpoint_validates_hosts(
        self,
        proxy_base_url: str,
        http_client: httpx.Client,
    ):
        """Test that _fetch endpoint rejects disallowed hosts."""
        response = http_client.post(
            f"{proxy_base_url}/_fetch",
            headers={"Content-Type": "application/json"},
            json={"url": "https://evil.example.com/malicious"},
        )
        
        # Should be rejected with 403
        assert response.status_code == 403, (
            f"Expected 403 for disallowed host, got {response.status_code} - {response.text[:500]}"
        )

    def test_fetch_endpoint_requires_url(
        self,
        proxy_base_url: str,
        http_client: httpx.Client,
    ):
        """Test that _fetch endpoint requires URL in body."""
        response = http_client.post(
            f"{proxy_base_url}/_fetch",
            headers={"Content-Type": "application/json"},
            json={},
        )
        
        # Should be rejected (400 for missing URL, or 403 if path rejected first)
        assert response.status_code in (400, 403), (
            f"Expected 400 or 403 for missing URL, got {response.status_code} - {response.text[:500]}"
        )


# =============================================================================
# Integration Test: Full Workflow
# =============================================================================

class TestFullWorkflow:
    """Integration tests for complete workflows through the proxy."""

    def test_auth_and_list_runs_workflow(
        self,
        proxy_base_url: str,
        neptune_token: str,
        neptune_project: str,
        http_client: httpx.Client,
    ):
        """Test complete auth + list runs workflow."""
        # Step 1: Exchange token
        token_response = http_client.get(
            f"{proxy_base_url}/api/backend/v1/authorization/oauth-token",
            headers={"X-Neptune-Api-Token": neptune_token},
        )
        
        assert token_response.status_code == 200, "Token exchange failed"
        access_token = token_response.json()["accessToken"]
        
        # Step 2: Get project info
        project_response = http_client.get(
            f"{proxy_base_url}/api/backend/v1/projects/get",
            params={"projectIdentifier": neptune_project},
            headers={"Authorization": f"Bearer {access_token}"},
        )
        
        assert project_response.status_code == 200, "Get project info failed"
        project_info = project_response.json()
        
        # Step 3: List runs
        runs_response = http_client.post(
            f"{proxy_base_url}/api/leaderboard/v1/leaderboard/entries/search",
            params={"projectIdentifier": neptune_project, "type": "run"},
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
            },
            json={"pagination": {"limit": 5, "offset": 0}},
        )
        
        assert runs_response.status_code == 200, "List runs failed"
        runs_data = runs_response.json()
        
        # Verify workflow completed successfully
        print(f"\n[+] Workflow completed successfully:")
        print(f"    Project: {project_info['name']}")
        print(f"    Organization: {project_info.get('organizationName', 'N/A')}")
        print(f"    Runs found: {len(runs_data.get('entries', []))}")


# =============================================================================
# Main entry point for standalone execution
# =============================================================================

if __name__ == "__main__":
    # Quick smoke test when run directly
    import sys
    
    print("Running Neptune Proxy Smoke Test...")
    print("=" * 60)
    
    try:
        supabase_url = get_supabase_url()
        neptune_token = get_neptune_token()
        neptune_project = get_neptune_project()
    except Exception as e:
        print(f"[!] Configuration error: {e}")
        print("\nPlease set the required environment variables:")
        print("  export SUPABASE_URL='http://localhost:54321'  # or your Supabase URL")
        print("  export NEPTUNE_API_TOKEN='eyJ...'")
        print("  export NEPTUNE_PROJECT='workspace/project'")
        sys.exit(1)
    
    proxy_base_url = f"{supabase_url}/functions/v1/neptune-proxy"
    
    print(f"Supabase URL: {supabase_url}")
    print(f"Proxy URL: {proxy_base_url}")
    print(f"Neptune Project: {neptune_project}")
    print("-" * 60)
    
    with httpx.Client(timeout=30.0) as client:
        # Test 1: Token exchange
        print("\n[1] Testing token exchange...")
        try:
            response = client.get(
                f"{proxy_base_url}/api/backend/v1/authorization/oauth-token",
                headers={"X-Neptune-Api-Token": neptune_token},
            )
            response.raise_for_status()
            access_token = response.json()["accessToken"]
            print(f"    [OK] Got access token ({len(access_token)} chars)")
        except Exception as e:
            print(f"    [FAIL] {e}")
            sys.exit(1)
        
        # Test 2: Project info
        print("\n[2] Testing project info...")
        try:
            response = client.get(
                f"{proxy_base_url}/api/backend/v1/projects/get",
                params={"projectIdentifier": neptune_project},
                headers={"Authorization": f"Bearer {access_token}"},
            )
            response.raise_for_status()
            project_info = response.json()
            print(f"    [OK] Project: {project_info['name']}")
            print(f"         Org: {project_info.get('organizationName', 'N/A')}")
        except Exception as e:
            print(f"    [FAIL] {e}")
            sys.exit(1)
        
        # Test 3: List runs
        print("\n[3] Testing run listing...")
        try:
            response = client.post(
                f"{proxy_base_url}/api/leaderboard/v1/leaderboard/entries/search",
                params={"projectIdentifier": neptune_project, "type": "run"},
                headers={
                    "Authorization": f"Bearer {access_token}",
                    "Content-Type": "application/json",
                },
                json={"pagination": {"limit": 5, "offset": 0}},
            )
            response.raise_for_status()
            runs_data = response.json()
            entries = runs_data.get("entries", [])
            print(f"    [OK] Found {len(entries)} runs")
            for entry in entries[:3]:
                name = "(unnamed)"
                for attr in entry.get("attributes", []):
                    if attr.get("name") == "sys/name":
                        props = attr.get("stringProperties", {})
                        name = props.get("value", "(no value)")
                        break
                print(f"         - {entry.get('experimentId', 'N/A')[:8]}... : {name}")
        except Exception as e:
            print(f"    [FAIL] {e}")
            sys.exit(1)
        
        print("\n" + "=" * 60)
        print("[+] All smoke tests passed!")
        print("\nTo run the full test suite, use:")
        print("  pytest goodseed/tests/test_neptune_proxy.py -v")
