//! Integration tests for the `Site` trait and surrounding types.
//!
//! Uses a mock `Site` that doesn't need a real browser — we don't spin up
//! Chrome here. The goal is to verify:
//! - trait method dispatch works
//! - `detect_auth_failure` patterns map cleanly
//! - `SessionStatus` variants preserve their fields
//! - `Credentials::Debug` hygiene (secrets masked)
//!
//! For real-browser end-to-end verification, see each site crate's own
//! live tests (e.g. `tail-fin-twitter/tests/twitter_live.rs`).

use async_trait::async_trait;
use serde_json::Value;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;

use tail_fin_core::{
    AuthFailureKind, BrowserSession, Credentials, FailureIndicators, SessionStatus, Site, SiteError,
};

struct MockSite {
    refresh_calls: AtomicUsize,
    validate_status: std::sync::Mutex<SessionStatus>,
}

impl MockSite {
    fn new() -> Self {
        Self {
            refresh_calls: AtomicUsize::new(0),
            validate_status: std::sync::Mutex::new(SessionStatus::Valid),
        }
    }

    fn set_validate(&self, s: SessionStatus) {
        *self.validate_status.lock().unwrap() = s;
    }

    fn refresh_call_count(&self) -> usize {
        self.refresh_calls.load(Ordering::SeqCst)
    }
}

#[async_trait]
impl Site for MockSite {
    fn id(&self) -> &'static str {
        "mock"
    }
    fn display_name(&self) -> &'static str {
        "Mock Site"
    }
    fn cookie_domain_patterns(&self) -> &'static [&'static str] {
        &[".mock.test"]
    }
    fn refresh_url(&self) -> &'static str {
        "https://mock.test/refresh"
    }
    fn refresh_interval_min(&self) -> Duration {
        Duration::from_millis(100)
    }

    async fn refresh(&self, _session: &BrowserSession) -> Result<Vec<Value>, SiteError> {
        self.refresh_calls.fetch_add(1, Ordering::SeqCst);
        Ok(vec![serde_json::json!({
            "name": "mock_token",
            "value": "v42",
            "domain": ".mock.test"
        })])
    }

    async fn validate(&self, _session: &BrowserSession) -> Result<SessionStatus, SiteError> {
        Ok(self.validate_status.lock().unwrap().clone())
    }

    fn detect_auth_failure(&self, ind: &FailureIndicators) -> Option<AuthFailureKind> {
        match ind.status {
            Some(401) => Some(AuthFailureKind::CookieExpired),
            Some(429) => Some(AuthFailureKind::RateLimited {
                retry_after: Duration::from_secs(60),
            }),
            _ => None,
        }
    }
}

// --- Site trait surface --------------------------------------------------

#[tokio::test]
async fn mock_site_has_expected_identity() {
    let site = MockSite::new();
    assert_eq!(site.id(), "mock");
    assert_eq!(site.display_name(), "Mock Site");
    assert_eq!(site.cookie_domain_patterns(), &[".mock.test"]);
    assert_eq!(site.refresh_url(), "https://mock.test/refresh");
}

#[test]
fn detect_auth_failure_maps_401_to_cookie_expired() {
    let site = MockSite::new();
    let ind = FailureIndicators {
        status: Some(401),
        ..Default::default()
    };
    let kind = site.detect_auth_failure(&ind).expect("expected kind");
    assert!(matches!(kind, AuthFailureKind::CookieExpired));
}

#[test]
fn detect_auth_failure_maps_429_to_rate_limited() {
    let site = MockSite::new();
    let ind = FailureIndicators {
        status: Some(429),
        ..Default::default()
    };
    let kind = site.detect_auth_failure(&ind).expect("expected kind");
    match kind {
        AuthFailureKind::RateLimited { retry_after } => {
            assert_eq!(retry_after, Duration::from_secs(60));
        }
        other => panic!("expected RateLimited, got {other:?}"),
    }
}

#[test]
fn detect_auth_failure_returns_none_for_200() {
    let site = MockSite::new();
    let ind = FailureIndicators {
        status: Some(200),
        ..Default::default()
    };
    assert!(site.detect_auth_failure(&ind).is_none());
}

// --- SessionStatus variants ----------------------------------------------

#[test]
fn session_status_valid() {
    let s = SessionStatus::Valid;
    assert!(matches!(s, SessionStatus::Valid));
}

#[test]
fn session_status_blocked_carries_retry_after() {
    let s = SessionStatus::Blocked {
        reason: "captcha".into(),
        retry_after: Some(Duration::from_secs(30)),
    };
    match s {
        SessionStatus::Blocked {
            reason,
            retry_after,
        } => {
            assert_eq!(reason, "captcha");
            assert_eq!(retry_after, Some(Duration::from_secs(30)));
        }
        _ => panic!("expected Blocked"),
    }
}

// --- Credentials debug hygiene -------------------------------------------

#[test]
fn credentials_debug_masks_password() {
    let c = Credentials::UsernamePassword {
        username: "user".into(),
        password: "verysecret".into(),
    };
    let debug = format!("{c:?}");
    assert!(!debug.contains("verysecret"));
}

// --- Mock state -----------------------------------------------------------

#[test]
fn mock_site_counts_start_at_zero() {
    let site = MockSite::new();
    assert_eq!(site.refresh_call_count(), 0);
}

#[test]
fn mock_site_validate_status_can_be_changed() {
    let site = MockSite::new();
    site.set_validate(SessionStatus::Expired);
    let guard = site.validate_status.lock().unwrap();
    match *guard {
        SessionStatus::Expired => {}
        _ => panic!("expected Expired"),
    }
}
