"""Tests for the cost tracking module."""

from __future__ import annotations

import tempfile
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from oclawma.costs import (
    AlertLevel,
    APICallCost,
    BudgetAlert,
    BudgetManager,
    CostBudget,
    CostReport,
    CostStore,
    CostTracker,
    CostTrackingError,
    JobCost,
    ReportPeriod,
)

# =============================================================================
# APICallCost Tests
# =============================================================================


class TestAPICallCost:
    """Tests for APICallCost dataclass."""

    def test_basic_creation(self):
        """Test creating an API call cost."""
        cost = APICallCost(
            provider="openai",
            model="gpt-4",
            cost=0.05,
            tokens_input=100,
            tokens_output=50,
        )
        assert cost.provider == "openai"
        assert cost.model == "gpt-4"
        assert cost.cost == 0.05
        assert cost.tokens_input == 100
        assert cost.tokens_output == 50
        assert isinstance(cost.timestamp, datetime)

    def test_to_dict(self):
        """Test converting to dictionary."""
        cost = APICallCost(
            provider="anthropic",
            model="claude-3",
            cost=0.03,
        )
        data = cost.to_dict()
        assert data["provider"] == "anthropic"
        assert data["model"] == "claude-3"
        assert data["cost"] == 0.03
        assert "timestamp" in data

    def test_from_dict(self):
        """Test creating from dictionary."""
        data = {
            "provider": "openai",
            "model": "gpt-3.5",
            "cost": 0.002,
            "tokens_input": 500,
            "tokens_output": 200,
            "timestamp": datetime.utcnow().isoformat(),
        }
        cost = APICallCost.from_dict(data)
        assert cost.provider == "openai"
        assert cost.cost == 0.002


# =============================================================================
# JobCost Tests
# =============================================================================


class TestJobCost:
    """Tests for JobCost model."""

    def test_basic_creation(self):
        """Test creating a job cost."""
        job = JobCost(job_id="job-123")
        assert job.job_id == "job-123"
        assert job.compute_time_seconds == 0.0
        assert job.api_costs == []
        assert job.storage_bytes == 0

    def test_creation_with_optional_fields(self):
        """Test creating with all optional fields."""
        job = JobCost(
            job_id="job-456",
            project_id="proj-1",
            tenant_id="tenant-1",
            compute_time_seconds=10.5,
            storage_bytes=1024,
            metadata={"key": "value"},
        )
        assert job.project_id == "proj-1"
        assert job.tenant_id == "tenant-1"
        assert job.compute_time_seconds == 10.5
        assert job.storage_bytes == 1024
        assert job.metadata == {"key": "value"}

    def test_add_api_cost(self):
        """Test adding API costs."""
        job = JobCost(job_id="job-123")
        job.add_api_cost("openai", "gpt-4", 0.05, 100, 50)
        job.add_api_cost("anthropic", "claude-3", 0.03, 200, 100)

        assert len(job.api_costs) == 2
        assert job.api_costs[0].provider == "openai"
        assert job.api_costs[1].provider == "anthropic"

    def test_total_api_cost(self):
        """Test total API cost calculation."""
        job = JobCost(job_id="job-123")
        job.add_api_cost("openai", "gpt-4", 0.05)
        job.add_api_cost("openai", "gpt-4", 0.03)

        assert job.total_api_cost == 0.08

    def test_total_tokens(self):
        """Test total tokens calculation."""
        job = JobCost(job_id="job-123")
        job.add_api_cost("openai", "gpt-4", 0.05, 100, 50)
        job.add_api_cost("openai", "gpt-4", 0.03, 200, 100)

        assert job.total_tokens == 450  # (100+50) + (200+100)

    def test_total_cost(self):
        """Test total cost calculation."""
        job = JobCost(
            job_id="job-123",
            compute_time_seconds=100.0,
            storage_bytes=int(1e9),  # 1 GB
        )
        job.add_api_cost("openai", "gpt-4", 0.10)

        total = job.total_cost
        assert total > 0.10  # API cost + compute + storage
        # Compute: 100 * 0.0001 = 0.01
        # Storage: 1GB * 0.023 / 30 = ~0.00077
        assert total == pytest.approx(0.11077, abs=0.001)

    def test_duration_seconds(self):
        """Test duration calculation."""
        created = datetime.utcnow()
        completed = created + timedelta(seconds=30)

        job = JobCost(
            job_id="job-123",
            created_at=created,
            completed_at=completed,
        )

        assert job.duration_seconds == 30.0

    def test_duration_seconds_not_completed(self):
        """Test duration when not completed."""
        job = JobCost(job_id="job-123")
        assert job.duration_seconds == 0.0

    def test_to_dict(self):
        """Test converting to dictionary."""
        job = JobCost(job_id="job-123", project_id="proj-1")
        job.add_api_cost("openai", "gpt-4", 0.05)

        data = job.to_dict()
        assert data["job_id"] == "job-123"
        assert data["project_id"] == "proj-1"
        assert "api_costs" in data
        assert "total_cost" in data

    def test_from_dict(self):
        """Test creating from dictionary."""
        data = {
            "job_id": "job-123",
            "project_id": "proj-1",
            "tenant_id": "tenant-1",
            "compute_time_seconds": 10.0,
            "api_costs": [
                {
                    "provider": "openai",
                    "model": "gpt-4",
                    "cost": 0.05,
                    "tokens_input": 100,
                    "tokens_output": 50,
                    "timestamp": datetime.utcnow().isoformat(),
                }
            ],
            "storage_bytes": 1024,
            "created_at": datetime.utcnow().isoformat(),
            "completed_at": None,
            "metadata": {},
        }
        job = JobCost.from_dict(data)
        assert job.job_id == "job-123"
        assert len(job.api_costs) == 1


# =============================================================================
# CostBudget Tests
# =============================================================================


class TestCostBudget:
    """Tests for CostBudget model."""

    def test_basic_creation(self):
        """Test creating a budget."""
        budget = CostBudget(id="budget-1", name="Test Budget")
        assert budget.id == "budget-1"
        assert budget.name == "Test Budget"
        assert budget.monthly_limit == 100.0
        assert budget.alert_thresholds == [50, 75, 90]

    def test_custom_thresholds(self):
        """Test custom alert thresholds."""
        budget = CostBudget(
            id="budget-1",
            name="Test",
            alert_thresholds=[25, 50, 75, 100],
        )
        assert budget.alert_thresholds == [25, 50, 75, 100]

    def test_invalid_threshold(self):
        """Test invalid threshold raises error."""
        with pytest.raises(ValueError, match="Threshold must be between 0 and 100"):
            CostBudget(id="budget-1", name="Test", alert_thresholds=[150])

    def test_check_threshold_no_alerts(self):
        """Test checking thresholds with low usage."""
        budget = CostBudget(id="budget-1", name="Test", monthly_limit=100.0)
        alerts = budget.check_threshold(30.0)  # 30% usage
        assert len(alerts) == 0

    def test_check_threshold_with_alerts(self):
        """Test checking thresholds with high usage."""
        budget = CostBudget(id="budget-1", name="Test", monthly_limit=100.0)
        alerts = budget.check_threshold(80.0)  # 80% usage

        assert len(alerts) == 2  # 50% and 75% thresholds
        assert alerts[0].level == AlertLevel.INFO
        assert alerts[1].level == AlertLevel.WARNING

    def test_check_threshold_critical(self):
        """Test critical alert at 90%+."""
        budget = CostBudget(id="budget-1", name="Test", monthly_limit=100.0)
        alerts = budget.check_threshold(95.0)  # 95% usage

        critical_alerts = [a for a in alerts if a.level == AlertLevel.CRITICAL]
        assert len(critical_alerts) >= 1

    def test_to_dict(self):
        """Test converting to dictionary."""
        budget = CostBudget(id="budget-1", name="Test Budget")
        data = budget.to_dict()
        assert data["id"] == "budget-1"
        assert data["name"] == "Test Budget"


# =============================================================================
# BudgetAlert Tests
# =============================================================================


class TestBudgetAlert:
    """Tests for BudgetAlert dataclass."""

    def test_creation(self):
        """Test creating an alert."""
        alert = BudgetAlert(
            level=AlertLevel.WARNING,
            message="Budget at 75%",
            budget_id="budget-1",
            current_usage=75.0,
            budget_limit=100.0,
        )
        assert alert.level == AlertLevel.WARNING
        assert alert.budget_id == "budget-1"
        assert alert.usage_percent == 75.0


# =============================================================================
# CostStore Tests
# =============================================================================


class TestCostStore:
    """Tests for CostStore."""

    @pytest.fixture
    def temp_store(self):
        """Create a temporary cost store."""
        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            path = Path(f.name)
        store = CostStore(path)
        yield store
        store.close()
        path.unlink(missing_ok=True)

    def test_save_and_get_job_cost(self, temp_store):
        """Test saving and retrieving job cost."""
        job = JobCost(
            job_id="job-123",
            project_id="proj-1",
            tenant_id="tenant-1",
            compute_time_seconds=10.0,
            storage_bytes=1024,
        )
        job.add_api_cost("openai", "gpt-4", 0.05, 100, 50)
        job.completed_at = datetime.utcnow()

        temp_store.save_job_cost(job)
        retrieved = temp_store.get_job_cost("job-123")

        assert retrieved is not None
        assert retrieved.job_id == "job-123"
        assert retrieved.project_id == "proj-1"
        assert retrieved.total_api_cost == 0.05

    def test_get_nonexistent_job(self, temp_store):
        """Test retrieving non-existent job."""
        result = temp_store.get_job_cost("nonexistent")
        assert result is None

    def test_get_job_costs_with_filters(self, temp_store):
        """Test querying job costs with filters."""
        # Create jobs
        job1 = JobCost(job_id="job-1", project_id="proj-a")
        job2 = JobCost(job_id="job-2", project_id="proj-b")
        job3 = JobCost(job_id="job-3", project_id="proj-a")

        temp_store.save_job_cost(job1)
        temp_store.save_job_cost(job2)
        temp_store.save_job_cost(job3)

        # Filter by project
        results = temp_store.get_job_costs(project_id="proj-a")
        assert len(results) == 2
        assert all(r.project_id == "proj-a" for r in results)

    def test_save_and_get_budget(self, temp_store):
        """Test saving and retrieving budget."""
        budget = CostBudget(
            id="budget-1",
            name="Test Budget",
            project_id="proj-1",
            monthly_limit=500.0,
        )

        temp_store.save_budget(budget)
        retrieved = temp_store.get_budget("budget-1")

        assert retrieved is not None
        assert retrieved.name == "Test Budget"
        assert retrieved.monthly_limit == 500.0

    def test_get_budgets_with_filters(self, temp_store):
        """Test querying budgets with filters."""
        budget1 = CostBudget(id="b1", name="B1", project_id="proj-a")
        budget2 = CostBudget(id="b2", name="B2", project_id="proj-b")
        budget3 = CostBudget(id="b3", name="B3", project_id="proj-a")

        temp_store.save_budget(budget1)
        temp_store.save_budget(budget2)
        temp_store.save_budget(budget3)

        results = temp_store.get_budgets(project_id="proj-a")
        assert len(results) == 2

    def test_delete_budget(self, temp_store):
        """Test deleting a budget."""
        budget = CostBudget(id="budget-1", name="Test")
        temp_store.save_budget(budget)

        assert temp_store.delete_budget("budget-1") is True
        assert temp_store.get_budget("budget-1") is None
        assert temp_store.delete_budget("budget-1") is False

    def test_save_alert(self, temp_store):
        """Test saving budget alerts."""
        budget = CostBudget(id="budget-1", name="Test")
        temp_store.save_budget(budget)

        alert = BudgetAlert(
            level=AlertLevel.WARNING,
            message="Budget at 75%",
            budget_id="budget-1",
            current_usage=75.0,
            budget_limit=100.0,
        )

        temp_store.save_alert(alert)
        # Alerts are saved, no direct retrieval method but no error

    def test_get_monthly_usage(self, temp_store):
        """Test calculating monthly usage."""
        # Create jobs with costs
        job1 = JobCost(job_id="job-1", project_id="proj-a")
        job1.add_api_cost("openai", "gpt-4", 0.50)
        job1.completed_at = datetime.utcnow()

        job2 = JobCost(job_id="job-2", project_id="proj-a")
        job2.add_api_cost("openai", "gpt-4", 0.30)
        job2.completed_at = datetime.utcnow()

        temp_store.save_job_cost(job1)
        temp_store.save_job_cost(job2)

        usage = temp_store.get_monthly_usage(project_id="proj-a")
        assert usage == pytest.approx(0.80, abs=0.01)


# =============================================================================
# CostTracker Tests
# =============================================================================


class TestCostTracker:
    """Tests for CostTracker."""

    @pytest.fixture
    def tracker(self):
        """Create an in-memory cost tracker."""
        tracker = CostTracker()
        yield tracker
        tracker.close()

    def test_start_job(self, tracker):
        """Test starting job tracking."""
        job = tracker.start_job("job-123", project_id="proj-1")
        assert job.job_id == "job-123"
        assert job.project_id == "proj-1"
        assert "job-123" in tracker._active_jobs

    def test_start_duplicate_job(self, tracker):
        """Test starting duplicate job raises error."""
        tracker.start_job("job-123")
        with pytest.raises(CostTrackingError, match="already being tracked"):
            tracker.start_job("job-123")

    def test_end_job(self, tracker):
        """Test ending job tracking."""
        tracker.start_job("job-123")
        import time

        time.sleep(0.01)  # Small delay for compute time
        job = tracker.end_job("job-123")

        assert job.job_id == "job-123"
        assert job.compute_time_seconds > 0
        assert job.completed_at is not None
        assert "job-123" not in tracker._active_jobs

    def test_end_nonexistent_job(self, tracker):
        """Test ending non-existent job raises error."""
        with pytest.raises(CostTrackingError, match="not being tracked"):
            tracker.end_job("nonexistent")

    def test_record_api_cost(self, tracker):
        """Test recording API costs."""
        tracker.start_job("job-123")
        tracker.record_api_cost("job-123", "openai", "gpt-4", 0.05, 100, 50)

        job = tracker.get_job_cost("job-123")
        assert len(job.api_costs) == 1
        assert job.api_costs[0].provider == "openai"

    def test_record_api_cost_nonexistent_job(self, tracker):
        """Test recording API cost for non-existent job raises error."""
        with pytest.raises(CostTrackingError, match="not being tracked"):
            tracker.record_api_cost("nonexistent", "openai", "gpt-4", 0.05)

    def test_record_storage(self, tracker):
        """Test recording storage usage."""
        tracker.start_job("job-123")
        tracker.record_storage("job-123", 1024)
        tracker.record_storage("job-123", 2048)

        job = tracker.get_job_cost("job-123")
        assert job.storage_bytes == 3072

    def test_track_job_context_manager(self, tracker):
        """Test track_job context manager."""
        with tracker.track_job("job-123", project_id="proj-1") as job:
            job.add_api_cost("openai", "gpt-4", 0.05)

        # Job should be saved after context exits
        saved = tracker.get_job_cost("job-123")
        assert saved is not None
        assert saved.total_api_cost == 0.05

    def test_track_job_context_manager_exception(self, tracker):
        """Test track_job handles exceptions."""
        try:
            with tracker.track_job("job-123") as job:
                job.add_api_cost("openai", "gpt-4", 0.05)
                raise ValueError("Test error")
        except ValueError:
            pass

        # Job should still be saved
        saved = tracker.get_job_cost("job-123")
        assert saved is not None

    def test_generate_report(self, tracker):
        """Test generating cost report."""
        # Create some job costs
        with tracker.track_job("job-1", project_id="proj-a") as job:
            job.add_api_cost("openai", "gpt-4", 0.50)

        with tracker.track_job("job-2", project_id="proj-b") as job:
            job.add_api_cost("anthropic", "claude-3", 0.30)

        report = tracker.generate_report(ReportPeriod.DAILY)

        assert report.period == ReportPeriod.DAILY
        assert report.total_jobs >= 2
        assert report.total_cost > 0
        assert "proj-a" in report.costs_by_project
        assert "proj-b" in report.costs_by_project

    def test_generate_report_by_provider(self, tracker):
        """Test provider breakdown in report."""
        with tracker.track_job("job-1") as job:
            job.add_api_cost("openai", "gpt-4", 0.50)

        with tracker.track_job("job-2") as job:
            job.add_api_cost("anthropic", "claude-3", 0.30)

        report = tracker.generate_report(ReportPeriod.DAILY)

        assert "openai" in report.costs_by_provider
        assert "anthropic" in report.costs_by_provider


# =============================================================================
# BudgetManager Tests
# =============================================================================


class TestBudgetManager:
    """Tests for BudgetManager."""

    @pytest.fixture
    def manager(self):
        """Create a budget manager with in-memory store."""
        store = CostStore()
        manager = BudgetManager(store)
        yield manager
        store.close()

    def test_create_budget(self, manager):
        """Test creating a budget."""
        budget = manager.create_budget(
            name="Test Budget",
            project_id="proj-1",
            monthly_limit=500.0,
        )

        assert budget.name == "Test Budget"
        assert budget.project_id == "proj-1"
        assert budget.monthly_limit == 500.0
        assert budget.id.startswith("budget-")

    def test_create_budget_with_custom_id(self, manager):
        """Test creating budget with custom ID."""
        budget = manager.create_budget(
            name="Test",
            budget_id="my-custom-id",
        )
        assert budget.id == "my-custom-id"

    def test_get_budget(self, manager):
        """Test retrieving a budget."""
        created = manager.create_budget(name="Test", budget_id="b1")
        retrieved = manager.get_budget("b1")

        assert retrieved is not None
        assert retrieved.id == created.id

    def test_list_budgets(self, manager):
        """Test listing budgets."""
        manager.create_budget(name="B1", project_id="proj-a", budget_id="b1")
        manager.create_budget(name="B2", project_id="proj-b", budget_id="b2")
        manager.create_budget(name="B3", project_id="proj-a", budget_id="b3")

        all_budgets = manager.list_budgets()
        assert len(all_budgets) == 3

        proj_a = manager.list_budgets(project_id="proj-a")
        assert len(proj_a) == 2

    def test_update_budget(self, manager):
        """Test updating a budget."""
        manager.create_budget(name="Original", monthly_limit=100.0, budget_id="b1")

        updated = manager.update_budget(
            "b1",
            name="Updated",
            monthly_limit=200.0,
        )

        assert updated is not None
        assert updated.name == "Updated"
        assert updated.monthly_limit == 200.0

    def test_update_nonexistent_budget(self, manager):
        """Test updating non-existent budget."""
        result = manager.update_budget("nonexistent", name="New")
        assert result is None

    def test_delete_budget(self, manager):
        """Test deleting a budget."""
        manager.create_budget(name="Test", budget_id="b1")
        assert manager.delete_budget("b1") is True
        assert manager.get_budget("b1") is None

    def test_check_budget_no_alerts(self, manager):
        """Test checking budget with no alerts."""
        manager.create_budget(
            name="Test",
            budget_id="b1",
            monthly_limit=100.0,
            alert_thresholds=[50, 75, 90],
        )

        # No usage yet, no alerts
        alerts = manager.check_budget("b1")
        assert len(alerts) == 0

    def test_check_budget_with_alerts(self, manager):
        """Test checking budget that triggers alerts."""
        # Create budget
        manager.create_budget(
            name="Test",
            project_id="proj-1",
            budget_id="b1",
            monthly_limit=100.0,
            alert_thresholds=[50, 75, 90],
        )

        # Add job costs to trigger alerts
        job = JobCost(job_id="job-1", project_id="proj-1")
        job.add_api_cost("openai", "gpt-4", 80.0)  # 80% of budget
        job.completed_at = datetime.utcnow()
        manager.store.save_job_cost(job)

        alerts = manager.check_budget("b1")
        assert len(alerts) >= 1
        assert any(a.level == AlertLevel.WARNING for a in alerts)

    def test_check_all_budgets(self, manager):
        """Test checking all budgets."""
        manager.create_budget(name="B1", project_id="proj-a", budget_id="b1")
        manager.create_budget(name="B2", project_id="proj-b", budget_id="b2")

        results = manager.check_all_budgets()
        assert isinstance(results, dict)

    def test_get_budget_usage(self, manager):
        """Test getting budget usage."""
        manager.create_budget(
            name="Test",
            project_id="proj-1",
            budget_id="b1",
            monthly_limit=100.0,
        )

        # Add job cost
        job = JobCost(job_id="job-1", project_id="proj-1")
        job.add_api_cost("openai", "gpt-4", 50.0)
        job.completed_at = datetime.utcnow()
        manager.store.save_job_cost(job)

        usage = manager.get_budget_usage("b1")
        assert usage is not None
        assert usage["current_usage"] == pytest.approx(50.0, abs=0.01)
        assert usage["monthly_limit"] == 100.0
        assert usage["usage_percent"] == 50.0

    def test_get_budget_usage_nonexistent(self, manager):
        """Test getting usage for non-existent budget."""
        usage = manager.get_budget_usage("nonexistent")
        assert usage is None

    def test_alert_callback(self):
        """Test alert callback is triggered."""
        callback_mock = MagicMock()
        store = CostStore()
        manager = BudgetManager(store, alert_callback=callback_mock)

        manager.create_budget(
            name="Test",
            project_id="proj-1",
            budget_id="b1",
            monthly_limit=100.0,
            alert_thresholds=[50],
        )

        # Add job cost to trigger alert
        job = JobCost(job_id="job-1", project_id="proj-1")
        job.add_api_cost("openai", "gpt-4", 60.0)
        job.completed_at = datetime.utcnow()
        manager.store.save_job_cost(job)

        manager.check_budget("b1")

        # Callback should have been called
        callback_mock.assert_called()
        store.close()


# =============================================================================
# CostReport Tests
# =============================================================================


class TestCostReport:
    """Tests for CostReport."""

    def test_to_dict(self):
        """Test converting report to dictionary."""
        report = CostReport(
            period=ReportPeriod.DAILY,
            start_date=datetime.utcnow(),
            end_date=datetime.utcnow(),
            total_cost=100.0,
            total_jobs=5,
            costs_by_project={"proj-a": 60.0, "proj-b": 40.0},
            costs_by_tenant={},
            costs_by_provider={"openai": 100.0},
            top_jobs=[],
            daily_breakdown={},
        )

        data = report.to_dict()
        assert data["period"] == "daily"
        assert data["total_cost"] == 100.0
        assert data["total_jobs"] == 5

    def test_to_json(self):
        """Test converting report to JSON."""
        report = CostReport(
            period=ReportPeriod.WEEKLY,
            start_date=datetime.utcnow(),
            end_date=datetime.utcnow(),
            total_cost=50.0,
            total_jobs=3,
            costs_by_project={},
            costs_by_tenant={},
            costs_by_provider={},
            top_jobs=[],
            daily_breakdown={},
        )

        json_str = report.to_json()
        assert "weekly" in json_str
        assert "50.0" in json_str

    def test_to_csv(self, tmp_path):
        """Test exporting report to CSV."""
        report = CostReport(
            period=ReportPeriod.MONTHLY,
            start_date=datetime.utcnow(),
            end_date=datetime.utcnow(),
            total_cost=200.0,
            total_jobs=10,
            costs_by_project={"proj-a": 120.0, "proj-b": 80.0},
            costs_by_tenant={},
            costs_by_provider={"openai": 150.0, "anthropic": 50.0},
            top_jobs=[],
            daily_breakdown={"2024-01-01": 50.0, "2024-01-02": 75.0},
        )

        output_dir = tmp_path / "report"
        report.to_csv(output_dir)

        assert (output_dir / "summary.csv").exists()
        assert (output_dir / "by_project.csv").exists()
        assert (output_dir / "by_provider.csv").exists()
        assert (output_dir / "daily.csv").exists()


# =============================================================================
# Integration Tests
# =============================================================================


class TestCostTrackingIntegration:
    """Integration tests for cost tracking."""

    def test_full_workflow(self):
        """Test complete cost tracking workflow."""
        # Create tracker
        tracker = CostTracker()

        # Track a job
        with tracker.track_job(
            job_id="integration-job-1",
            project_id="integration-test",
            tenant_id="tenant-1",
        ) as job:
            # Simulate API calls
            job.add_api_cost("openai", "gpt-4", 0.05, 1000, 500)
            job.add_api_cost("openai", "gpt-4", 0.03, 600, 300)

        # Verify job was saved
        saved_job = tracker.get_job_cost("integration-job-1")
        assert saved_job is not None
        assert saved_job.total_api_cost == 0.08
        assert saved_job.total_tokens == 2400

        # Create budget with a lower limit to trigger alerts
        budget_manager = BudgetManager(tracker.store)
        budget = budget_manager.create_budget(
            name="Integration Test Budget",
            project_id="integration-test",
            monthly_limit=0.1,  # Lower limit to trigger 80% alert
            alert_thresholds=[50, 80],
        )

        # Check budget (should trigger alert at 80% - $0.08 out of $0.10 = 80%)
        alerts = budget_manager.check_budget(budget.id)
        assert len(alerts) >= 1

        # Generate report
        report = tracker.generate_report(
            period=ReportPeriod.DAILY,
            project_id="integration-test",
        )
        assert report.total_jobs >= 1
        assert report.total_cost > 0

        tracker.close()

    def test_multi_project_tracking(self):
        """Test tracking costs across multiple projects."""
        tracker = CostTracker()

        # Create jobs for different projects
        for i in range(3):
            with tracker.track_job(f"job-proj-a-{i}", project_id="proj-a") as job:
                job.add_api_cost("openai", "gpt-4", 0.10)

        for i in range(2):
            with tracker.track_job(f"job-proj-b-{i}", project_id="proj-b") as job:
                job.add_api_cost("anthropic", "claude-3", 0.08)

        # Generate report
        report = tracker.generate_report(ReportPeriod.DAILY)

        assert report.costs_by_project.get("proj-a", 0) > 0
        assert report.costs_by_project.get("proj-b", 0) > 0
        assert report.costs_by_provider.get("openai", 0) > 0
        assert report.costs_by_provider.get("anthropic", 0) > 0

        tracker.close()
