"""Tests for reputation system (scores, streaks, tiers, social sharing)."""

import json
from datetime import date, timedelta
from unittest.mock import MagicMock, patch

from deepsweep.reputation.api_client import (
    OfflineScorer,
    ReputationAPIClient,
    ShareResponse,
)
from deepsweep.reputation.local_store import LocalReputationData, LocalReputationStore


class TestLocalReputationStore:
    """Test local reputation storage (JSON persistence)."""

    def test_initial_load_returns_empty_data(self, tmp_path, monkeypatch):
        """New installations should start with empty reputation data."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()
        data = store.load()

        assert data.current_score == 0
        assert data.tier == "unverified"
        assert data.streak_days == 0
        assert data.total_validations == 0

    def test_save_and_load_persists_data(self, tmp_path, monkeypatch):
        """Saved reputation data should persist between loads."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        data = LocalReputationData(
            current_score=85,
            tier="gold",
            streak_days=5,
            total_validations=10,
            last_grade="B",
        )
        store.save(data)

        loaded = store.load()
        assert loaded.current_score == 85
        assert loaded.tier == "gold"
        assert loaded.streak_days == 5
        assert loaded.total_validations == 10
        assert loaded.last_grade == "B"

    def test_update_after_validation_increments_count(self, tmp_path, monkeypatch):
        """Each validation should increment total_validations."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        # First validation
        store.update_after_validation("A", 100, "diamond", 1)
        data = store.load()
        assert data.total_validations == 1

        # Second validation
        store.update_after_validation("A", 100, "diamond", 2)
        data = store.load()
        assert data.total_validations == 2

    def test_update_sets_first_validation_date(self, tmp_path, monkeypatch):
        """First validation should set first_validation_date."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        data = store.update_after_validation("A", 100, "diamond", 1)
        assert data.first_validation_date is not None
        assert data.first_validation_date == date.today().isoformat()

        # Second validation should not change first_validation_date
        first_date = data.first_validation_date
        data = store.update_after_validation("A", 100, "diamond", 2)
        assert data.first_validation_date == first_date

    def test_calculate_new_streak_first_validation(self, tmp_path, monkeypatch):
        """First validation should start a 1-day streak."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        streak = store.calculate_new_streak()
        assert streak == 1

    def test_calculate_new_streak_consecutive_days(self, tmp_path, monkeypatch):
        """Validating on consecutive days should extend streak."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        # Day 1
        yesterday = (date.today() - timedelta(days=1)).isoformat()
        data = LocalReputationData(streak_days=1, streak_last_date=yesterday)
        store.save(data)

        # Day 2 - should extend streak
        streak = store.calculate_new_streak()
        assert streak == 2

    def test_calculate_new_streak_same_day(self, tmp_path, monkeypatch):
        """Multiple validations on same day don't increase streak."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        # Same day
        today = date.today().isoformat()
        data = LocalReputationData(streak_days=5, streak_last_date=today)
        store.save(data)

        # Same day validation - streak stays same
        streak = store.calculate_new_streak()
        assert streak == 5

    def test_calculate_new_streak_broken(self, tmp_path, monkeypatch):
        """Missing a day should reset streak to 1."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        # 3 days ago (streak broken)
        three_days_ago = (date.today() - timedelta(days=3)).isoformat()
        data = LocalReputationData(streak_days=10, streak_last_date=three_days_ago)
        store.save(data)

        # Today - streak resets
        streak = store.calculate_new_streak()
        assert streak == 1

    def test_get_streak_status_validated_today(self, tmp_path, monkeypatch):
        """get_streak_status should detect if validated today."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        today = date.today().isoformat()
        data = LocalReputationData(streak_days=5, streak_last_date=today)
        store.save(data)

        streak_days, validated_today = store.get_streak_status()
        assert streak_days == 5
        assert validated_today is True

    def test_get_streak_status_not_validated_today(self, tmp_path, monkeypatch):
        """get_streak_status should detect if not validated today."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        yesterday = (date.today() - timedelta(days=1)).isoformat()
        data = LocalReputationData(streak_days=3, streak_last_date=yesterday)
        store.save(data)

        streak_days, validated_today = store.get_streak_status()
        assert streak_days == 3
        assert validated_today is False

    def test_get_streak_status_broken(self, tmp_path, monkeypatch):
        """get_streak_status should detect broken streaks."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        three_days_ago = (date.today() - timedelta(days=3)).isoformat()
        data = LocalReputationData(streak_days=10, streak_last_date=three_days_ago)
        store.save(data)

        streak_days, validated_today = store.get_streak_status()
        assert streak_days == 0
        assert validated_today is False

    def test_clear_removes_file(self, tmp_path, monkeypatch):
        """clear() should remove reputation file."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        # Save some data
        data = LocalReputationData(current_score=100)
        store.save(data)
        assert store.reputation_file.exists()

        # Clear
        store.clear()
        assert not store.reputation_file.exists()

    def test_corrupted_file_returns_empty_data(self, tmp_path, monkeypatch):
        """Corrupted JSON should return empty data (graceful degradation)."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        store = LocalReputationStore()

        # Write corrupted JSON
        store.reputation_file.write_text("not valid json{{{")

        # Should return empty data without crashing
        data = store.load()
        assert data.current_score == 0


class TestOfflineScorer:
    """Test offline score calculation."""

    def test_calculate_score_perfect_a(self):
        """Grade A with 0 findings = 100."""
        score = OfflineScorer.calculate_score("A", 0)
        assert score == 100

    def test_calculate_score_a_with_findings(self):
        """Grade A with findings = 100 - (findings * 2)."""
        assert OfflineScorer.calculate_score("A", 1) == 98
        assert OfflineScorer.calculate_score("A", 5) == 90

    def test_calculate_score_b(self):
        """Grade B scoring."""
        assert OfflineScorer.calculate_score("B", 0) == 85
        assert OfflineScorer.calculate_score("B", 1) == 82

    def test_calculate_score_c(self):
        """Grade C scoring."""
        assert OfflineScorer.calculate_score("C", 0) == 75
        assert OfflineScorer.calculate_score("C", 1) == 71

    def test_calculate_score_d(self):
        """Grade D scoring."""
        assert OfflineScorer.calculate_score("D", 0) == 65
        assert OfflineScorer.calculate_score("D", 1) == 60

    def test_calculate_score_f(self):
        """Grade F scoring."""
        assert OfflineScorer.calculate_score("F", 0) == 50
        assert OfflineScorer.calculate_score("F", 1) == 45

    def test_calculate_score_floor_zero(self):
        """Score should never go below 0."""
        score = OfflineScorer.calculate_score("F", 100)
        assert score == 0

    def test_calculate_score_ceiling_100(self):
        """Score should never exceed 100."""
        score = OfflineScorer.calculate_score("A", 0)
        assert score == 100

    def test_calculate_tier_diamond(self):
        """Score >= 95 = diamond."""
        assert OfflineScorer.calculate_tier(95) == "diamond"
        assert OfflineScorer.calculate_tier(100) == "diamond"

    def test_calculate_tier_platinum(self):
        """Score 90-94 = platinum."""
        assert OfflineScorer.calculate_tier(90) == "platinum"
        assert OfflineScorer.calculate_tier(94) == "platinum"

    def test_calculate_tier_gold(self):
        """Score 80-89 = gold."""
        assert OfflineScorer.calculate_tier(80) == "gold"
        assert OfflineScorer.calculate_tier(89) == "gold"

    def test_calculate_tier_silver(self):
        """Score 70-79 = silver."""
        assert OfflineScorer.calculate_tier(70) == "silver"
        assert OfflineScorer.calculate_tier(79) == "silver"

    def test_calculate_tier_bronze(self):
        """Score 60-69 = bronze."""
        assert OfflineScorer.calculate_tier(60) == "bronze"
        assert OfflineScorer.calculate_tier(69) == "bronze"

    def test_calculate_tier_unverified(self):
        """Score < 60 = unverified."""
        assert OfflineScorer.calculate_tier(0) == "unverified"
        assert OfflineScorer.calculate_tier(59) == "unverified"


class TestReputationAPIClient:
    """Test reputation API client."""

    def test_offline_mode_skips_api(self, tmp_path, monkeypatch):
        """Offline mode should skip all API calls."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        client = ReputationAPIClient(offline_mode=True)

        response = client.submit_validation("A", 0, ["CURSOR-RULES-001"])

        # Should use offline scoring
        assert response.score == 100
        assert response.tier == "diamond"
        assert response.global_rank is None  # No API call made

    def test_submit_validation_updates_local_store(self, tmp_path, monkeypatch):
        """submit_validation should update local store."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        client = ReputationAPIClient(offline_mode=True)

        client.submit_validation("B", 2, ["CURSOR-RULES-001", "MCP-POISON-001"])

        # Check local store was updated
        data = client.store.load()
        assert data.current_score > 0
        assert data.tier in ["bronze", "silver", "gold", "platinum", "diamond"]
        assert data.total_validations == 1
        assert data.last_grade == "B"

    @patch("deepsweep.reputation.api_client.urlopen")
    def test_submit_validation_with_api_success(
        self, mock_urlopen, tmp_path, monkeypatch
    ):
        """API success should use server-calculated values."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )

        # Mock successful API response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "status": "success",
                "score": 92,
                "tier": "platinum",
                "streak_days": 7,
                "global_rank": 123,
                "percentile": 85,
            }
        ).encode()
        mock_response.__enter__.return_value = mock_response
        mock_urlopen.return_value = mock_response

        client = ReputationAPIClient(offline_mode=False)
        response = client.submit_validation("A", 1, ["CURSOR-RULES-001"])

        # Should use API values
        assert response.score == 92
        assert response.tier == "platinum"
        assert response.streak_days == 7
        assert response.global_rank == 123
        assert response.percentile == 85

    @patch("deepsweep.reputation.api_client.urlopen")
    def test_submit_validation_api_failure_fallback(
        self, mock_urlopen, tmp_path, monkeypatch
    ):
        """API failure should fall back to offline scoring."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )

        # Mock API failure
        mock_urlopen.side_effect = Exception("Network error")

        client = ReputationAPIClient(offline_mode=False)
        response = client.submit_validation("A", 0, [])

        # Should fall back to offline calculation
        assert response.score == 100
        assert response.tier == "diamond"
        assert response.global_rank is None

    def test_generate_share_offline_fallback(self, tmp_path, monkeypatch):
        """generate_share should work offline."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        client = ReputationAPIClient(offline_mode=True)

        response = client.generate_share("A", 100, 5)

        assert isinstance(response, ShareResponse)
        assert "100" in response.share_text
        assert "Grade A" in response.share_text
        assert "twitter.com" in response.share_url

    @patch("deepsweep.reputation.api_client.urlopen")
    def test_generate_share_with_api(self, mock_urlopen, tmp_path, monkeypatch):
        """generate_share should use API when available."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )

        # Mock API response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "status": "success",
                "share_url": "https://deepsweep.ai/share/abc123",
                "share_text": "Custom share text",
                "badge_url": "https://deepsweep.ai/badge/abc123.svg",
            }
        ).encode()
        mock_response.__enter__.return_value = mock_response
        mock_urlopen.return_value = mock_response

        client = ReputationAPIClient(offline_mode=False)
        response = client.generate_share("A", 100, 5)

        assert "abc123" in response.share_url
        assert response.share_text == "Custom share text"

    def test_get_leaderboard_offline_returns_empty(self, tmp_path, monkeypatch):
        """Offline mode should return empty leaderboard."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        client = ReputationAPIClient(offline_mode=True)

        leaderboard = client.get_leaderboard()
        assert leaderboard == []

    @patch("deepsweep.reputation.api_client.urlopen")
    def test_get_leaderboard_with_api(self, mock_urlopen, tmp_path, monkeypatch):
        """API should return leaderboard data."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )

        # Mock API response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "status": "success",
                "leaderboard": [
                    {"rank": 1, "score": 100, "tier": "diamond"},
                    {"rank": 2, "score": 95, "tier": "diamond"},
                ],
            }
        ).encode()
        mock_response.__enter__.return_value = mock_response
        mock_urlopen.return_value = mock_response

        client = ReputationAPIClient(offline_mode=False)
        leaderboard = client.get_leaderboard()

        assert len(leaderboard) == 2
        assert leaderboard[0]["rank"] == 1


class TestReputationIntegration:
    """Integration tests for reputation system."""

    def test_full_workflow_offline(self, tmp_path, monkeypatch):
        """Test complete workflow in offline mode."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )
        client = ReputationAPIClient(offline_mode=True)

        # First validation
        response1 = client.submit_validation("A", 0, [])
        assert response1.score == 100
        assert response1.tier == "diamond"
        assert response1.streak_days == 1

        # Check local store
        data = client.store.load()
        assert data.total_validations == 1
        assert data.streak_days == 1

        # Share
        share = client.generate_share("A", response1.score, response1.streak_days)
        assert "100" in share.share_text

    def test_streak_persistence_across_sessions(self, tmp_path, monkeypatch):
        """Streaks should persist across different client instances."""
        monkeypatch.setattr(
            "deepsweep.reputation.local_store.LocalReputationStore.CONFIG_DIR",
            tmp_path,
        )

        # Session 1
        client1 = ReputationAPIClient(offline_mode=True)
        response1 = client1.submit_validation("A", 0, [])
        assert response1.streak_days == 1

        # Session 2 (new client instance, same day)
        client2 = ReputationAPIClient(offline_mode=True)
        response2 = client2.submit_validation("A", 0, [])
        assert response2.streak_days == 1  # Same day = same streak
