//! Tests for el-publish.

#[cfg(test)]
mod tests {
    use std::time::{SystemTime, UNIX_EPOCH};

    use crate::{
        cert::{CertInfo, CertStore},
        config::{AppleConfig, GoogleConfig, PublishConfig, RolloutConfig},
        metadata::{LocaleMetadata, StoreMetadata},
        rollout::{RolloutDecision, RolloutMonitor, RolloutState},
        screenshot::{ScreenshotCapture, ScreenshotTarget, StubScreenshotCapture},
        PublishError,
    };

    fn unix_now() -> u64 {
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0)
    }

    fn test_config() -> PublishConfig {
        PublishConfig::new("1.2.0", 42)
            .with_apple(AppleConfig::new(
                "will@example.com",
                "TEAM123",
                "ai.example.myapp",
            ))
            .with_google(GoogleConfig::new("./secrets/google.json", "ai.example.myapp"))
    }

    // ── Test 1: PublishConfig parses version and build number ─────────────────
    #[test]
    fn test_publish_config_basic() {
        let cfg = test_config();
        assert_eq!(cfg.version, "1.2.0");
        assert_eq!(cfg.build_number, 42);
        assert!(cfg.apple.is_some());
        assert!(cfg.google.is_some());
    }

    // ── Test 2: AppleConfig has correct fields ────────────────────────────────
    #[test]
    fn test_apple_config() {
        let cfg = AppleConfig::new("will@example.com", "TEAM123", "ai.example.myapp")
            .with_category("utilities");
        assert_eq!(cfg.account, "will@example.com");
        assert_eq!(cfg.team_id, "TEAM123");
        assert_eq!(cfg.category, "utilities");
    }

    // ── Test 3: GoogleConfig track defaults to internal ───────────────────────
    #[test]
    fn test_google_config_default_track() {
        let cfg = GoogleConfig::new("./google.json", "ai.example.myapp");
        assert_eq!(cfg.track, "internal");
    }

    // ── Test 4: GoogleConfig with_track ──────────────────────────────────────
    #[test]
    fn test_google_config_track() {
        let cfg = GoogleConfig::new("./google.json", "ai.example.myapp")
            .with_track("production");
        assert_eq!(cfg.track, "production");
    }

    // ── Test 5: RolloutConfig default values ─────────────────────────────────
    #[test]
    fn test_rollout_config_default() {
        let cfg = RolloutConfig::default();
        assert_eq!(cfg.initial_percent, 100);
        assert!(!cfg.auto_advance);
        assert_eq!(cfg.max_crash_rate, 0.01);
    }

    // ── Test 6: RolloutMonitor evaluates normal state as Maintain ─────────────
    #[test]
    fn test_rollout_maintain() {
        let cfg = RolloutConfig::new(10).with_auto_advance(24);
        let monitor = RolloutMonitor::new(cfg);
        let state = RolloutState {
            track: "production".into(),
            current_percent: 10,
            crash_rate: 0.001,  // well below threshold
            is_halted: false,
            hours_elapsed: 5,   // less than 24h advance threshold
        };
        let decision = monitor.evaluate(&state);
        assert!(matches!(decision, RolloutDecision::Maintain));
    }

    // ── Test 7: RolloutMonitor halts on high crash rate ───────────────────────
    #[test]
    fn test_rollout_halt_on_crash_rate() {
        let cfg = RolloutConfig::new(10);
        let monitor = RolloutMonitor::new(cfg);
        let state = RolloutState {
            track: "production".into(),
            current_percent: 10,
            crash_rate: 0.05,   // 5% > 1% threshold
            is_halted: false,
            hours_elapsed: 2,
        };
        let decision = monitor.evaluate(&state);
        assert!(matches!(decision, RolloutDecision::Halt { .. }));
    }

    // ── Test 8: RolloutMonitor advances after threshold hours ─────────────────
    #[test]
    fn test_rollout_advance() {
        let cfg = RolloutConfig::new(10).with_auto_advance(24);
        let monitor = RolloutMonitor::new(cfg);
        let state = RolloutState {
            track: "production".into(),
            current_percent: 10,
            crash_rate: 0.001,
            is_halted: false,
            hours_elapsed: 25,  // exceeds 24h threshold
        };
        let decision = monitor.evaluate(&state);
        assert!(matches!(decision, RolloutDecision::Advance { to_percent: 25 }));
    }

    // ── Test 9: RolloutMonitor advance progression 10→25→50→100 ──────────────
    #[test]
    fn test_rollout_advance_progression() {
        let cfg = RolloutConfig::new(10).with_auto_advance(1);
        let monitor = RolloutMonitor::new(cfg);

        let mut state = RolloutState {
            track: "production".into(),
            current_percent: 10,
            crash_rate: 0.001,
            is_halted: false,
            hours_elapsed: 5,
        };

        let expected = [25u8, 50, 100];
        for expected_next in expected {
            let decision = monitor.evaluate(&state);
            if let RolloutDecision::Advance { to_percent } = decision {
                state.current_percent = to_percent;
                state.hours_elapsed = 5;
                assert_eq!(to_percent, expected_next);
            } else {
                panic!("expected Advance decision, got {:?}", decision);
            }
        }
    }

    // ── Test 10: CertInfo::days_until_expiry ─────────────────────────────────
    #[test]
    fn test_cert_days_until_expiry() {
        let future = unix_now() + 30 * 86400; // 30 days from now
        let cert = CertInfo::new("My Cert", "TEAM123", "SN001", "Distribution", future);
        let days = cert.days_until_expiry();
        assert!(days >= 29 && days <= 30, "should be ~30 days: {}", days);
    }

    // ── Test 11: CertInfo::is_expired for past cert ───────────────────────────
    #[test]
    fn test_cert_is_expired() {
        let past = unix_now() - 86400; // yesterday
        let cert = CertInfo::new("Old Cert", "TEAM123", "SN002", "Distribution", past);
        assert!(cert.is_expired());
        assert_eq!(cert.days_until_expiry(), 0);
    }

    // ── Test 12: CertStore::renewal_warnings finds expiring certs ─────────────
    #[test]
    fn test_cert_store_renewal_warnings() {
        let mut store = CertStore::new().with_warn_days(45);
        let soon = unix_now() + 10 * 86400; // 10 days away
        store.add(CertInfo::new("My Cert", "T1", "SN1", "Distribution", soon));
        let warnings = store.renewal_warnings();
        assert!(!warnings.is_empty(), "should have warnings for cert expiring in 10 days");
        assert!(warnings[0].contains("EXPIRING SOON") || warnings[0].contains("EXPIRED"));
    }

    // ── Test 13: CertStore::validate_for_distribution fails without cert ───────
    #[test]
    fn test_cert_store_validate_no_cert() {
        let store = CertStore::new();
        let result = store.validate_for_distribution("TEAM123");
        assert!(result.is_err());
        match result {
            Err(PublishError::Certificate(_)) => {}
            _ => panic!("expected Certificate error"),
        }
    }

    // ── Test 14: StoreMetadata::for_locale fallback to en-US ──────────────────
    #[test]
    fn test_store_metadata_locale_fallback() {
        let meta = StoreMetadata::new()
            .add_locale(
                "en-US",
                LocaleMetadata::new("My App", "The best app ever"),
            );
        // de-DE not set, should fall back to en-US
        let locale_meta = meta.for_locale("de-DE").unwrap();
        assert_eq!(locale_meta.title, "My App");
    }

    // ── Test 15: StoreMetadata::load_from_dir with missing dir is ok ──────────
    #[test]
    fn test_store_metadata_missing_dir() {
        // Missing directory should not error — returns empty metadata
        let result = StoreMetadata::load_from_dir("/nonexistent/path/that/doesnt/exist");
        assert!(result.is_ok());
    }

    // ── Test 16: ScreenshotTarget::all_standard has expected targets ──────────
    #[test]
    fn test_screenshot_targets() {
        let targets = ScreenshotTarget::all_standard();
        assert!(targets.len() >= 4, "should have at least 4 standard targets");
        let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
        assert!(ids.contains(&"iphone-6.7"));
        assert!(ids.contains(&"android-phone"));
    }

    // ── Test 17: ScreenshotTarget::find by ID ────────────────────────────────
    #[test]
    fn test_screenshot_target_find() {
        let target = ScreenshotTarget::find("ipad-13").unwrap();
        assert_eq!(target.name, "iPad 13\"");
        assert_eq!(target.width, 2064);
    }

    // ── Test 18: StubScreenshotCapture produces paths ─────────────────────────
    #[test]
    fn test_stub_screenshot_capture() {
        let capture = StubScreenshotCapture;
        let target = ScreenshotTarget::find("iphone-6.7").unwrap();
        let shot = capture.capture("home_screen", &target, "en-US", "/tmp/screenshots").unwrap();
        assert!(shot.path.contains("iphone-6.7"));
        assert!(shot.path.contains("home_screen"));
        assert_eq!(shot.locale, "en-US");
    }

    // ── Test 19: PublishConfig with rollout ───────────────────────────────────
    #[test]
    fn test_publish_config_with_rollout() {
        let rollout = RolloutConfig::new(10)
            .with_auto_advance(24)
            .with_max_crash_rate(0.005);
        let cfg = PublishConfig::new("1.0.0", 1).with_rollout(rollout);
        let r = cfg.rollout.unwrap();
        assert_eq!(r.initial_percent, 10);
        assert!(r.auto_advance);
        assert_eq!(r.max_crash_rate, 0.005);
    }

    // ── Test 20: PublishConfig screenshot_targets ─────────────────────────────
    #[test]
    fn test_publish_config_screenshot_targets() {
        let cfg = PublishConfig::new("1.0.0", 1)
            .with_screenshot_targets(vec!["iphone-6.7", "android-phone"]);
        assert_eq!(cfg.screenshot_targets.len(), 2);
        assert!(cfg.screenshot_targets.contains(&"iphone-6.7".to_string()));
    }
}
