/*!
 * Caching space weather provider that automatically downloads and caches space weather data.
 */

use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;

use crate::space_weather::file_provider::{FileSpaceWeatherProvider, PACKAGED_SW_FILE};
use crate::space_weather::provider::SpaceWeatherProvider;
use crate::space_weather::types::{SpaceWeatherExtrapolation, SpaceWeatherType};
use crate::time::{Epoch, TimeSystem};
use crate::utils::BraheError;
use crate::utils::atomic_write;
use crate::utils::cache::get_space_weather_cache_dir;
use crate::utils::download::download_to_file;

/// Default URL for downloading space weather data from CelesTrak
const DEFAULT_SW_URL: &str = "https://celestrak.org/SpaceData/sw19571001.txt";

/// Default filename for cached space weather data
const DEFAULT_SW_FILENAME: &str = "sw19571001.txt";

/// CachingSpaceWeatherProvider automatically downloads and caches space weather data.
///
/// This provider wraps a `FileSpaceWeatherProvider` and handles automatic downloading
/// and caching of space weather data from CelesTrak. It checks the age of the cached
/// file and re-downloads if it's older than the specified maximum age.
///
/// # Fields
///
/// - `inner`: Internal FileSpaceWeatherProvider that actually loads and provides the data
/// - `cache_path`: Path to the cached file
/// - `max_age`: Maximum age of cached file in seconds
/// - `auto_refresh`: If true, automatically check file age on each access and refresh if needed
/// - `extrapolate`: Extrapolation behavior for out-of-range requests
/// - `file_loaded_at`: Timestamp when the file was last loaded
///
/// # Example
///
/// ```no_run
/// use brahe::space_weather::{CachingSpaceWeatherProvider, SpaceWeatherProvider, SpaceWeatherExtrapolation};
///
/// // Create provider with default cache location and 7-day max age
/// let sw = CachingSpaceWeatherProvider::new(
///     None,
///     7 * 86400,  // 7 days
///     false,      // auto_refresh
///     SpaceWeatherExtrapolation::Hold,
/// ).unwrap();
///
/// assert!(sw.is_initialized());
/// ```
#[derive(Clone)]
pub struct CachingSpaceWeatherProvider {
    /// Inner file provider
    inner: Arc<Mutex<FileSpaceWeatherProvider>>,
    /// Path to cached file
    cache_path: PathBuf,
    /// Maximum age of cached file in seconds
    max_age: u64,
    /// Enable automatic refresh checks on each data access. If true, provider verifies
    /// file age before each query and downloads updates when needed. If false, manual refresh required.
    pub auto_refresh: bool,
    /// Extrapolation behavior
    extrapolate: SpaceWeatherExtrapolation,
    /// Timestamp when file was loaded
    file_loaded_at: Arc<Mutex<SystemTime>>,
}

impl CachingSpaceWeatherProvider {
    /// Creates a new `CachingSpaceWeatherProvider`.
    ///
    /// If the file doesn't exist, it will be downloaded. If the file exists but is older than
    /// `max_age`, it will be re-downloaded before loading. Otherwise, the existing file
    /// is loaded.
    ///
    /// # Arguments
    ///
    /// * `cache_dir` - Optional custom cache directory. Defaults to `~/.cache/brahe/space_weather/`
    /// * `max_age` - Maximum age of cached file in seconds before re-downloading
    /// * `auto_refresh` - If true, automatically check file age on each access and refresh if needed
    /// * `extrapolate` - Extrapolation behavior for out-of-range requests
    ///
    /// # Returns
    ///
    /// * `Result<CachingSpaceWeatherProvider, BraheError>` - Provider or error
    ///
    /// # Example
    ///
    /// ```no_run
    /// use brahe::space_weather::{CachingSpaceWeatherProvider, SpaceWeatherExtrapolation};
    ///
    /// // With default cache location
    /// let provider = CachingSpaceWeatherProvider::new(
    ///     None,
    ///     7 * 86400, // 7 days
    ///     false,     // auto_refresh
    ///     SpaceWeatherExtrapolation::Hold
    /// ).unwrap();
    ///
    /// // With custom cache directory
    /// use std::path::PathBuf;
    /// let provider = CachingSpaceWeatherProvider::new(
    ///     Some(PathBuf::from("/tmp/sw_cache")),
    ///     7 * 86400,
    ///     true,      // auto_refresh enabled
    ///     SpaceWeatherExtrapolation::Hold
    /// ).unwrap();
    /// ```
    pub fn new(
        cache_dir: Option<PathBuf>,
        max_age: u64,
        auto_refresh: bool,
        extrapolate: SpaceWeatherExtrapolation,
    ) -> Result<Self, BraheError> {
        // Determine cache directory
        let cache_dir = match cache_dir {
            Some(dir) => dir,
            None => PathBuf::from(get_space_weather_cache_dir()?),
        };

        let cache_path = cache_dir.join(DEFAULT_SW_FILENAME);

        // If the cache file is missing, seed it from the compiled-in bundled data so
        // that fresh environments (CI runners, containers, new installs) can initialize
        // offline without requiring an immediate network download. The subsequent age
        // check still triggers a refresh download if the seeded data is stale.
        if !cache_path.exists() {
            Self::seed_from_bundled(&cache_path)?;
        }

        // Check if we need to download or update the file
        let needs_download = Self::check_file_age(&cache_path, max_age)?;

        if needs_download {
            download_space_weather(&cache_path)?;
        }

        let inner = FileSpaceWeatherProvider::from_file(&cache_path, extrapolate)?;

        // Record when file was loaded
        let file_loaded_at = Arc::new(Mutex::new(SystemTime::now()));

        Ok(Self {
            inner: Arc::new(Mutex::new(inner)),
            cache_path,
            max_age,
            auto_refresh,
            extrapolate,
            file_loaded_at,
        })
    }

    /// Creates a `CachingSpaceWeatherProvider` with a custom URL.
    ///
    /// # Arguments
    ///
    /// * `url` - URL to download space weather data from
    /// * `cache_dir` - Optional custom cache directory
    /// * `max_age` - Maximum age of cached file in seconds
    /// * `auto_refresh` - If true, automatically check file age on each access
    /// * `extrapolate` - Extrapolation behavior
    pub fn with_url(
        url: &str,
        cache_dir: Option<PathBuf>,
        max_age: u64,
        auto_refresh: bool,
        extrapolate: SpaceWeatherExtrapolation,
    ) -> Result<Self, BraheError> {
        let cache_dir = match cache_dir {
            Some(dir) => dir,
            None => PathBuf::from(get_space_weather_cache_dir()?),
        };

        let cache_path = cache_dir.join(DEFAULT_SW_FILENAME);

        // Seed a missing cache file from bundled data so initialization can succeed
        // offline (see `new` for details).
        if !cache_path.exists() {
            Self::seed_from_bundled(&cache_path)?;
        }

        // Check if we need to download
        let needs_download = Self::check_file_age(&cache_path, max_age)?;

        if needs_download {
            download_from_url(url, &cache_path)?;
        }

        let inner = FileSpaceWeatherProvider::from_file(&cache_path, extrapolate)?;

        // Record when file was loaded
        let file_loaded_at = Arc::new(Mutex::new(SystemTime::now()));

        Ok(Self {
            inner: Arc::new(Mutex::new(inner)),
            cache_path,
            max_age,
            auto_refresh,
            extrapolate,
            file_loaded_at,
        })
    }

    /// Checks if a file needs to be downloaded based on its age.
    ///
    /// Returns `true` if:
    /// - The file doesn't exist
    /// - The file's modification time cannot be determined
    /// - The file is older than `max_age`
    ///
    /// # Arguments
    ///
    /// * `filepath` - Path to check
    /// * `max_age` - Maximum acceptable age in seconds
    ///
    /// # Returns
    ///
    /// * `Ok(true)` - File needs to be downloaded
    /// * `Ok(false)` - File exists and is current
    /// * `Err(BraheError)` - Error checking file
    fn check_file_age(filepath: &Path, max_age: u64) -> Result<bool, BraheError> {
        // If file doesn't exist, we need to download it
        if !filepath.exists() {
            return Ok(true);
        }

        // Get file metadata
        let metadata = fs::metadata(filepath).map_err(|e| {
            BraheError::IoError(format!(
                "Failed to get metadata for {}: {}",
                filepath.display(),
                e
            ))
        })?;

        // Get file modification time
        let modified = metadata.modified().map_err(|e| {
            BraheError::IoError(format!(
                "Failed to get modification time for {}: {}",
                filepath.display(),
                e
            ))
        })?;

        // Calculate file age in seconds
        let age = SystemTime::now()
            .duration_since(modified)
            .map_err(|e| {
                BraheError::IoError(format!(
                    "Failed to calculate file age for {}: {}",
                    filepath.display(),
                    e
                ))
            })?
            .as_secs();

        // Return true if file is older than max age
        Ok(age > max_age)
    }

    /// Seeds a missing cache file from the compiled-in bundled space weather data.
    ///
    /// This allows `CachingSpaceWeatherProvider` to initialize offline on a fresh
    /// cache. The seeded file is written with the current modification time, so it is
    /// treated as current by `check_file_age` and only refreshed once it exceeds
    /// `max_age`.
    ///
    /// # Arguments
    ///
    /// * `cache_path` - Path where the bundled data should be written
    fn seed_from_bundled(cache_path: &Path) -> Result<(), BraheError> {
        atomic_write(cache_path, PACKAGED_SW_FILE).map_err(|e| {
            BraheError::IoError(format!(
                "Failed to seed space weather cache file {} from bundled data: {}",
                cache_path.display(),
                e
            ))
        })
    }

    /// Refreshes the cached data by re-checking the file age and reloading if necessary.
    ///
    /// This method allows manual refresh of the cache without creating a new provider instance.
    ///
    /// # Returns
    ///
    /// * `Ok(())` - Refresh succeeded (or wasn't needed)
    /// * `Err(BraheError)` - Refresh failed
    ///
    /// # Example
    ///
    /// ```no_run
    /// use brahe::space_weather::{CachingSpaceWeatherProvider, SpaceWeatherExtrapolation};
    ///
    /// let provider = CachingSpaceWeatherProvider::new(
    ///     None,
    ///     7 * 86400,
    ///     false,
    ///     SpaceWeatherExtrapolation::Hold
    /// ).unwrap();
    ///
    /// // Later, force a refresh check
    /// provider.refresh().unwrap();
    /// ```
    pub fn refresh(&self) -> Result<(), BraheError> {
        let needs_download = Self::check_file_age(&self.cache_path, self.max_age)?;

        if needs_download {
            download_space_weather(&self.cache_path)?;
            let new_provider =
                FileSpaceWeatherProvider::from_file(&self.cache_path, self.extrapolate)?;
            *self.inner.lock().unwrap() = new_provider;
            *self.file_loaded_at.lock().unwrap() = SystemTime::now();
        }

        Ok(())
    }

    /// Returns the path to the cached file.
    pub fn cache_path(&self) -> &Path {
        &self.cache_path
    }

    /// Returns the Epoch when the space weather file was last loaded into memory, in UTC.
    ///
    /// This represents the timestamp when the file was last loaded into memory.
    ///
    /// # Returns
    ///
    /// * `Epoch` - Epoch representing when the file was loaded, in UTC time system
    ///
    /// # Example
    ///
    /// ```no_run
    /// use brahe::space_weather::{CachingSpaceWeatherProvider, SpaceWeatherExtrapolation};
    ///
    /// let provider = CachingSpaceWeatherProvider::new(
    ///     None,
    ///     7 * 86400,
    ///     false,
    ///     SpaceWeatherExtrapolation::Hold
    /// ).unwrap();
    ///
    /// let file_epoch = provider.file_epoch();
    /// println!("Space weather file loaded at: {}", file_epoch);
    /// ```
    pub fn file_epoch(&self) -> Epoch {
        let system_time = *self.file_loaded_at.lock().unwrap();

        // Convert SystemTime to Epoch
        // SystemTime is based on UNIX epoch (1970-01-01 00:00:00 UTC)
        let duration_since_unix_epoch = system_time
            .duration_since(SystemTime::UNIX_EPOCH)
            .expect("System time is before UNIX epoch");

        let seconds_since_unix = duration_since_unix_epoch.as_secs_f64();

        // UNIX epoch in MJD: 1970-01-01 00:00:00 UTC = MJD 40587.0
        const UNIX_EPOCH_MJD: f64 = 40587.0;
        let mjd = UNIX_EPOCH_MJD + seconds_since_unix / 86400.0;

        Epoch::from_mjd(mjd, TimeSystem::UTC)
    }

    /// Returns the age of the currently loaded space weather file in seconds.
    ///
    /// Calculates how many seconds have elapsed since the file was loaded.
    ///
    /// # Returns
    ///
    /// * `f64` - Age of the loaded file in seconds
    ///
    /// # Example
    ///
    /// ```no_run
    /// use brahe::space_weather::{CachingSpaceWeatherProvider, SpaceWeatherExtrapolation};
    ///
    /// let provider = CachingSpaceWeatherProvider::new(
    ///     None,
    ///     7 * 86400,
    ///     false,
    ///     SpaceWeatherExtrapolation::Hold
    /// ).unwrap();
    ///
    /// let age_seconds = provider.file_age();
    /// println!("Space weather file age: {:.2} seconds", age_seconds);
    /// ```
    pub fn file_age(&self) -> f64 {
        let system_time = *self.file_loaded_at.lock().unwrap();
        let now = SystemTime::now();

        let duration = now
            .duration_since(system_time)
            .expect("System time went backwards");

        duration.as_secs_f64()
    }

    /// Checks if auto-refresh is needed and performs it if necessary.
    ///
    /// This is an internal method called by SpaceWeatherProvider trait methods
    /// when auto_refresh is enabled.
    fn check_auto_refresh(&self) -> Result<(), BraheError> {
        if self.auto_refresh {
            self.refresh()?;
        }
        Ok(())
    }
}

impl SpaceWeatherProvider for CachingSpaceWeatherProvider {
    fn len(&self) -> usize {
        self.inner.lock().unwrap().len()
    }

    fn sw_type(&self) -> SpaceWeatherType {
        self.inner.lock().unwrap().sw_type()
    }

    fn is_initialized(&self) -> bool {
        self.inner.lock().unwrap().is_initialized()
    }

    fn extrapolation(&self) -> SpaceWeatherExtrapolation {
        self.inner.lock().unwrap().extrapolation()
    }

    fn mjd_min(&self) -> f64 {
        self.inner.lock().unwrap().mjd_min()
    }

    fn mjd_max(&self) -> f64 {
        self.inner.lock().unwrap().mjd_max()
    }

    fn mjd_last_observed(&self) -> f64 {
        self.inner.lock().unwrap().mjd_last_observed()
    }

    fn mjd_last_daily_predicted(&self) -> f64 {
        self.inner.lock().unwrap().mjd_last_daily_predicted()
    }

    fn mjd_last_monthly_predicted(&self) -> f64 {
        self.inner.lock().unwrap().mjd_last_monthly_predicted()
    }

    fn get_kp(&self, mjd: f64) -> Result<f64, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_kp(mjd)
    }

    fn get_kp_all(&self, mjd: f64) -> Result<[f64; 8], BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_kp_all(mjd)
    }

    fn get_kp_daily(&self, mjd: f64) -> Result<f64, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_kp_daily(mjd)
    }

    fn get_ap(&self, mjd: f64) -> Result<f64, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_ap(mjd)
    }

    fn get_ap_all(&self, mjd: f64) -> Result<[f64; 8], BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_ap_all(mjd)
    }

    fn get_ap_daily(&self, mjd: f64) -> Result<f64, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_ap_daily(mjd)
    }

    fn get_f107_observed(&self, mjd: f64) -> Result<f64, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_f107_observed(mjd)
    }

    fn get_f107_adjusted(&self, mjd: f64) -> Result<f64, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_f107_adjusted(mjd)
    }

    fn get_f107_obs_avg81(&self, mjd: f64) -> Result<f64, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_f107_obs_avg81(mjd)
    }

    fn get_f107_adj_avg81(&self, mjd: f64) -> Result<f64, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_f107_adj_avg81(mjd)
    }

    fn get_sunspot_number(&self, mjd: f64) -> Result<u32, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_sunspot_number(mjd)
    }

    fn get_last_kp(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_last_kp(mjd, n)
    }

    fn get_last_ap(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_last_ap(mjd, n)
    }

    fn get_last_daily_kp(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_last_daily_kp(mjd, n)
    }

    fn get_last_daily_ap(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_last_daily_ap(mjd, n)
    }

    fn get_last_f107(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_last_f107(mjd, n)
    }

    fn get_last_kpap_epochs(&self, mjd: f64, n: usize) -> Result<Vec<Epoch>, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_last_kpap_epochs(mjd, n)
    }

    fn get_last_daily_epochs(&self, mjd: f64, n: usize) -> Result<Vec<Epoch>, BraheError> {
        self.check_auto_refresh()?;
        self.inner.lock().unwrap().get_last_daily_epochs(mjd, n)
    }
}

/// Download space weather data from the default CelesTrak URL.
fn download_space_weather(output_path: &Path) -> Result<(), BraheError> {
    download_from_url(DEFAULT_SW_URL, output_path)
}

/// Download data from a URL to a file, retrying transient failures with exponential
/// backoff and jitter.
fn download_from_url(url: &str, output_path: &Path) -> Result<(), BraheError> {
    download_to_file(url, "space weather", output_path)
}

#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
    use super::*;
    use crate::utils::testing::get_test_space_weather_filepath;
    use std::fs::File;
    use std::path::PathBuf;
    use std::thread;
    use std::time::Duration;
    use tempfile::TempDir;

    /// Helper to set up a cache directory with test space weather data.
    fn setup_test_cache(cache_dir: &PathBuf) {
        let test_file = get_test_space_weather_filepath();
        let cache_path = cache_dir.join(DEFAULT_SW_FILENAME);
        fs::create_dir_all(cache_dir).unwrap();
        fs::copy(&test_file, &cache_path).unwrap();
    }

    #[test]
    fn test_check_file_age_nonexistent() {
        let dir = TempDir::new().unwrap();
        let filepath = dir.path().join("nonexistent.txt");

        // Non-existent file should need download
        assert!(CachingSpaceWeatherProvider::check_file_age(&filepath, 86400).unwrap());
    }

    #[test]
    fn test_check_file_age_current() {
        let dir = TempDir::new().unwrap();
        let filepath = dir.path().join("current.txt");

        // Create a new file
        File::create(&filepath).unwrap();

        // File should be current (less than 1 day old)
        assert!(!CachingSpaceWeatherProvider::check_file_age(&filepath, 86400).unwrap());
    }

    #[test]
    #[cfg_attr(not(feature = "integration"), ignore)]
    fn test_check_file_age_stale() {
        let dir = TempDir::new().unwrap();
        let filepath = dir.path().join("stale.txt");

        // Create a file
        File::create(&filepath).unwrap();

        // Sleep briefly to ensure some time passes
        thread::sleep(Duration::from_secs(2));

        // Check with a very small max age (file should be stale)
        assert!(CachingSpaceWeatherProvider::check_file_age(&filepath, 1).unwrap());
    }

    #[test]
    fn test_new_seeds_from_bundled_when_missing() {
        // A missing cache file should be seeded from the compiled-in bundled data,
        // allowing initialization to succeed offline (no network required).
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();
        let cache_path = cache_dir.join(DEFAULT_SW_FILENAME);

        assert!(!cache_path.exists());

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            7 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        // File was created from bundled data and provider is populated, all offline.
        assert!(cache_path.exists());
        assert!(provider.is_initialized());
        assert!(provider.len() > 0);
    }

    #[test]
    fn test_caching_provider_from_packaged() {
        // Test that we can create a provider even without network access
        // by using the packaged default file
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Create with max_age of 0 to force download attempt, but it should
        // fall back to packaged data if download fails
        let result = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            0,
            false,
            SpaceWeatherExtrapolation::Hold,
        );

        // Even if download fails, we should get a valid provider from packaged data
        // However, in CI/test environments the download might succeed or fail
        // So we just verify the creation attempt works
        assert!(result.is_ok() || result.is_err());
    }

    #[test]
    fn test_file_age_and_epoch() {
        // Create a provider from packaged data
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400, // 1 year max age
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        // File age should be very small (just loaded)
        let age = provider.file_age();
        assert!(
            age < 1.0,
            "File age should be less than 1 second, got {}",
            age
        );

        // File epoch should be close to now
        let epoch = provider.file_epoch();
        let now_mjd = Epoch::now().mjd();
        let epoch_mjd = epoch.mjd();
        assert!(
            (now_mjd - epoch_mjd).abs() < 1.0 / 86400.0, // Within 1 second
            "File epoch MJD {} should be close to now {}",
            epoch_mjd,
            now_mjd
        );
    }

    #[test]
    fn test_refresh() {
        // Create a provider from packaged data
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        let original_len = provider.len();

        // Refresh should succeed (no download needed since file is current)
        provider.refresh().unwrap();

        // Length should be unchanged
        assert_eq!(provider.len(), original_len);
    }

    #[test]
    fn test_provider_delegation() {
        // Test that SpaceWeatherProvider methods are properly delegated
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        // Test basic properties
        assert!(provider.is_initialized());
        assert_eq!(provider.sw_type(), SpaceWeatherType::CssiSpaceWeather);
        assert_eq!(provider.extrapolation(), SpaceWeatherExtrapolation::Hold);
        assert!(provider.len() > 0);

        // Test data retrieval - use MJD within data range
        let test_mjd = 60000.0; // Recent date that should be in the file

        let kp = provider.get_kp(test_mjd).unwrap();
        assert!((0.0..=9.0).contains(&kp));

        let ap = provider.get_ap_daily(test_mjd).unwrap();
        assert!((0.0..=400.0).contains(&ap));

        let f107 = provider.get_f107_observed(test_mjd).unwrap();
        assert!(f107 > 0.0);

        let isn = provider.get_sunspot_number(test_mjd).unwrap();
        assert!(isn < 500);
    }

    #[test]
    fn test_mjd_boundaries() {
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        // Test mjd_min
        assert_eq!(provider.mjd_min(), 36112.0);

        // Test mjd_max
        assert!(provider.mjd_max() > 60000.0);

        // Test mjd_last_observed
        assert!(provider.mjd_last_observed() > 60000.0);

        // Test mjd_last_daily_predicted
        assert!(provider.mjd_last_daily_predicted() >= provider.mjd_last_observed());
        assert!(provider.mjd_last_daily_predicted() > 58849.0);

        // Test mjd_last_monthly_predicted
        assert!(provider.mjd_last_monthly_predicted() >= provider.mjd_last_daily_predicted());
        assert!(provider.mjd_last_monthly_predicted() > 58849.0);
    }

    #[test]
    fn test_get_f107_adj_avg81() {
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        let mjd = 60000.0;
        let f107_adj_avg = provider.get_f107_adj_avg81(mjd).unwrap();
        assert!(f107_adj_avg > 0.0);
        assert!(f107_adj_avg > 50.0 && f107_adj_avg < 400.0);
    }

    #[test]
    fn test_get_last_methods() {
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        let mjd = 60000.0;

        // Test get_last_kp
        let kp_values = provider.get_last_kp(mjd, 5).unwrap();
        assert_eq!(kp_values.len(), 5);
        for kp in &kp_values {
            assert!((0.0..=9.0).contains(kp));
        }

        // Test get_last_ap
        let ap_values = provider.get_last_ap(mjd, 5).unwrap();
        assert_eq!(ap_values.len(), 5);
        for ap in &ap_values {
            assert!(*ap >= 0.0);
        }

        // Test get_last_daily_kp
        let daily_kp = provider.get_last_daily_kp(mjd, 3).unwrap();
        assert_eq!(daily_kp.len(), 3);
        for kp in &daily_kp {
            assert!(*kp >= 0.0 && *kp <= 9.0);
        }

        // Test get_last_daily_ap
        let daily_ap = provider.get_last_daily_ap(mjd, 3).unwrap();
        assert_eq!(daily_ap.len(), 3);
        for ap in &daily_ap {
            assert!(*ap >= 0.0);
        }

        // Test get_last_f107
        let f107_values = provider.get_last_f107(mjd, 3).unwrap();
        assert_eq!(f107_values.len(), 3);
        for f107 in &f107_values {
            assert!(*f107 > 0.0);
        }

        // Test get_last_kpap_epochs
        let epochs = provider.get_last_kpap_epochs(mjd, 5).unwrap();
        assert_eq!(epochs.len(), 5);
        for i in 0..epochs.len() - 1 {
            assert!(epochs[i].mjd() < epochs[i + 1].mjd());
        }

        // Test get_last_daily_epochs
        let daily_epochs = provider.get_last_daily_epochs(mjd, 3).unwrap();
        assert_eq!(daily_epochs.len(), 3);
        for i in 0..daily_epochs.len() - 1 {
            assert!(daily_epochs[i].mjd() < daily_epochs[i + 1].mjd());
        }
    }

    #[test]
    fn test_with_url_from_cached_file() {
        // Test with_url when file already exists in cache (no network call needed)
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory as if it was downloaded
        setup_test_cache(&cache_dir);

        // Create provider with a dummy URL - since file exists and is fresh, no download occurs
        let provider = CachingSpaceWeatherProvider::with_url(
            "https://example.com/sw19571001.txt",
            Some(cache_dir),
            365 * 86400, // 1 year max age
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        // Verify provider was created successfully
        assert!(provider.is_initialized());
        assert!(provider.len() > 0);
        assert_eq!(provider.sw_type(), SpaceWeatherType::CssiSpaceWeather);
        assert_eq!(provider.extrapolation(), SpaceWeatherExtrapolation::Hold);

        // Verify data can be retrieved
        let test_mjd = 60000.0;
        let kp = provider.get_kp(test_mjd).unwrap();
        assert!((0.0..=9.0).contains(&kp));

        let ap = provider.get_ap_daily(test_mjd).unwrap();
        assert!(ap >= 0.0);

        let f107 = provider.get_f107_observed(test_mjd).unwrap();
        assert!(f107 > 0.0);
    }

    #[test]
    fn test_get_kp_all() {
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        let mjd = 60000.0;
        let kp_all = provider.get_kp_all(mjd).unwrap();
        assert_eq!(kp_all.len(), 8);
        for kp in kp_all.iter() {
            assert!((0.0..=9.0).contains(kp));
        }
    }

    #[test]
    fn test_get_kp_daily() {
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        let mjd = 60000.0;
        let kp_daily = provider.get_kp_daily(mjd).unwrap();
        assert!((0.0..=9.0).contains(&kp_daily));
    }

    #[test]
    fn test_get_ap() {
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        let mjd = 60000.0;
        let ap = provider.get_ap(mjd).unwrap();
        assert!(ap >= 0.0);
    }

    #[test]
    fn test_get_ap_all() {
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        let mjd = 60000.0;
        let ap_all = provider.get_ap_all(mjd).unwrap();
        assert_eq!(ap_all.len(), 8);
        for ap in ap_all.iter() {
            assert!(*ap >= 0.0);
        }
    }

    #[test]
    fn test_get_f107_adjusted() {
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        let mjd = 60000.0;
        let f107_adj = provider.get_f107_adjusted(mjd).unwrap();
        assert!(f107_adj >= 0.0);
    }

    #[test]
    fn test_get_f107_obs_avg81() {
        let temp_dir = TempDir::new().unwrap();
        let cache_dir = temp_dir.path().to_path_buf();

        // Copy test data to cache directory
        setup_test_cache(&cache_dir);

        let provider = CachingSpaceWeatherProvider::new(
            Some(cache_dir),
            365 * 86400,
            false,
            SpaceWeatherExtrapolation::Hold,
        )
        .unwrap();

        let mjd = 60000.0;
        let f107_obs_avg = provider.get_f107_obs_avg81(mjd).unwrap();
        assert!(f107_obs_avg > 0.0);
    }
}
