fmp_rs/endpoints/
bulk.rs

1//! Bulk data endpoints - Framework implementation without heavy downloads
2
3use crate::{
4    client::FmpClient,
5    error::Result,
6    models::bulk::{
7        BulkDataInfo, BulkEarningsEstimate, BulkEtfHolding, BulkFinancialStatement,
8        BulkHistoricalPricesMeta, BulkInsiderTrade, BulkInstitutionalHolding, BulkStockPrice,
9    },
10};
11use serde::Serialize;
12
13/// Bulk Data API endpoints
14///
15/// Note: These endpoints provide access to large datasets. In production,
16/// consider implementing streaming, chunking, or selective downloading
17/// to manage memory usage and network bandwidth effectively.
18pub struct Bulk {
19    client: FmpClient,
20}
21
22impl Bulk {
23    pub(crate) fn new(client: FmpClient) -> Self {
24        Self { client }
25    }
26
27    /// Get bulk stock prices (all symbols)
28    ///
29    /// **Warning:** This endpoint returns data for ALL stocks and can be very large (100MB+).
30    /// Consider using get_bulk_prices_sample() for testing or implement pagination.
31    ///
32    /// # Example
33    /// ```no_run
34    /// # use fmp_rs::FmpClient;
35    /// # #[tokio::main]
36    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
37    /// let client = FmpClient::builder().api_key("your_api_key").build()?;
38    /// let bulk = client.bulk();
39    ///
40    /// // WARNING: This downloads ALL stock prices - can be 100MB+
41    /// // let all_prices = bulk.get_bulk_stock_prices().await?;
42    ///
43    /// // Use sample version for testing instead:
44    /// let sample_prices = bulk.get_bulk_prices_sample(100).await?;
45    /// # Ok(())
46    /// # }
47    /// ```
48    pub async fn get_bulk_stock_prices(&self) -> Result<Vec<BulkStockPrice>> {
49        #[derive(Serialize)]
50        struct Query<'a> {
51            apikey: &'a str,
52        }
53
54        let url = self.client.build_url("/v3/quotes/nyse");
55        self.client
56            .get_with_query(
57                &url,
58                &Query {
59                    apikey: self.client.api_key(),
60                },
61            )
62            .await
63    }
64
65    /// Get a sample of bulk stock prices (first N results)
66    ///
67    /// This provides a lightweight way to test the bulk prices endpoint
68    /// without downloading the entire dataset.
69    ///
70    /// # Arguments
71    /// * `limit` - Maximum number of price records to return
72    ///
73    /// # Example
74    /// ```no_run
75    /// # use fmp_rs::FmpClient;
76    /// # #[tokio::main]
77    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
78    /// let client = FmpClient::builder().api_key("your_api_key").build()?;
79    /// let bulk = client.bulk();
80    /// let sample = bulk.get_bulk_prices_sample(50).await?;
81    /// println!("Sample contains {} price records", sample.len());
82    /// # Ok(())
83    /// # }
84    /// ```
85    pub async fn get_bulk_prices_sample(&self, limit: usize) -> Result<Vec<BulkStockPrice>> {
86        let all_prices = self.get_bulk_stock_prices().await?;
87        Ok(all_prices.into_iter().take(limit).collect())
88    }
89
90    /// Get bulk financial statements for all companies
91    ///
92    /// **Warning:** This endpoint returns financial data for ALL companies and is extremely large.
93    ///
94    /// # Arguments
95    /// * `period` - "annual" or "quarter"
96    /// * `year` - Year for the financial data (optional)
97    ///
98    /// # Example
99    /// ```no_run
100    /// # use fmp_rs::FmpClient;
101    /// # #[tokio::main]
102    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
103    /// let client = FmpClient::builder().api_key("your_api_key").build()?;
104    /// let bulk = client.bulk();
105    ///
106    /// // Get sample of annual financial statements
107    /// let statements = bulk.get_bulk_financials_sample("annual", Some(2023), 10).await?;
108    /// # Ok(())
109    /// # }
110    /// ```
111    pub async fn get_bulk_financial_statements(
112        &self,
113        period: &str,
114        year: Option<i32>,
115    ) -> Result<Vec<BulkFinancialStatement>> {
116        #[derive(Serialize)]
117        struct Query<'a> {
118            period: &'a str,
119            apikey: &'a str,
120            #[serde(skip_serializing_if = "Option::is_none")]
121            year: Option<i32>,
122        }
123
124        let url = self.client.build_url("/v4/financial-statements-list");
125        self.client
126            .get_with_query(
127                &url,
128                &Query {
129                    period,
130                    apikey: self.client.api_key(),
131                    year,
132                },
133            )
134            .await
135    }
136
137    /// Get sample of bulk financial statements
138    ///
139    /// # Arguments
140    /// * `period` - "annual" or "quarter"
141    /// * `year` - Year for the financial data (optional)
142    /// * `limit` - Maximum number of records to return
143    pub async fn get_bulk_financials_sample(
144        &self,
145        period: &str,
146        year: Option<i32>,
147        limit: usize,
148    ) -> Result<Vec<BulkFinancialStatement>> {
149        let all_statements = self.get_bulk_financial_statements(period, year).await?;
150        Ok(all_statements.into_iter().take(limit).collect())
151    }
152
153    /// Get bulk ETF holdings data
154    ///
155    /// **Warning:** Contains holdings for ALL ETFs - can be very large.
156    ///
157    /// # Example
158    /// ```no_run
159    /// # use fmp_rs::FmpClient;
160    /// # #[tokio::main]
161    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
162    /// let client = FmpClient::builder().api_key("your_api_key").build()?;
163    /// let bulk = client.bulk();
164    /// let sample_holdings = bulk.get_bulk_etf_holdings_sample(50).await?;
165    /// # Ok(())
166    /// # }
167    /// ```
168    pub async fn get_bulk_etf_holdings(&self) -> Result<Vec<BulkEtfHolding>> {
169        #[derive(Serialize)]
170        struct Query<'a> {
171            apikey: &'a str,
172        }
173
174        let url = self.client.build_url("/v4/etf-holdings");
175        self.client
176            .get_with_query(
177                &url,
178                &Query {
179                    apikey: self.client.api_key(),
180                },
181            )
182            .await
183    }
184
185    /// Get sample of bulk ETF holdings
186    ///
187    /// # Arguments
188    /// * `limit` - Maximum number of holding records to return
189    pub async fn get_bulk_etf_holdings_sample(&self, limit: usize) -> Result<Vec<BulkEtfHolding>> {
190        let all_holdings = self.get_bulk_etf_holdings().await?;
191        Ok(all_holdings.into_iter().take(limit).collect())
192    }
193
194    /// Get bulk insider trading data
195    ///
196    /// **Warning:** Contains ALL insider trades - extremely large dataset.
197    ///
198    /// # Example
199    /// ```no_run
200    /// # use fmp_rs::FmpClient;
201    /// # #[tokio::main]
202    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
203    /// let client = FmpClient::builder().api_key("your_api_key").build()?;
204    /// let bulk = client.bulk();
205    /// let recent_trades = bulk.get_bulk_insider_trades_sample(25).await?;
206    /// # Ok(())
207    /// # }
208    /// ```
209    pub async fn get_bulk_insider_trades(&self) -> Result<Vec<BulkInsiderTrade>> {
210        #[derive(Serialize)]
211        struct Query<'a> {
212            apikey: &'a str,
213        }
214
215        let url = self.client.build_url("/v4/insider-trading-list");
216        self.client
217            .get_with_query(
218                &url,
219                &Query {
220                    apikey: self.client.api_key(),
221                },
222            )
223            .await
224    }
225
226    /// Get sample of bulk insider trading data
227    ///
228    /// # Arguments
229    /// * `limit` - Maximum number of trade records to return
230    pub async fn get_bulk_insider_trades_sample(
231        &self,
232        limit: usize,
233    ) -> Result<Vec<BulkInsiderTrade>> {
234        let all_trades = self.get_bulk_insider_trades().await?;
235        Ok(all_trades.into_iter().take(limit).collect())
236    }
237
238    /// Get bulk institutional holdings (13F filings)
239    ///
240    /// **Warning:** Contains ALL institutional holdings - massive dataset.
241    ///
242    /// # Arguments
243    /// * `date` - Filing date (YYYY-MM-DD format, optional)
244    ///
245    /// # Example
246    /// ```no_run
247    /// # use fmp_rs::FmpClient;
248    /// # #[tokio::main]
249    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
250    /// let client = FmpClient::builder().api_key("your_api_key").build()?;
251    /// let bulk = client.bulk();
252    /// let recent_holdings = bulk.get_bulk_institutional_holdings_sample(None, 30).await?;
253    /// # Ok(())
254    /// # }
255    /// ```
256    pub async fn get_bulk_institutional_holdings(
257        &self,
258        date: Option<&str>,
259    ) -> Result<Vec<BulkInstitutionalHolding>> {
260        #[derive(Serialize)]
261        struct Query<'a> {
262            apikey: &'a str,
263            #[serde(skip_serializing_if = "Option::is_none")]
264            date: Option<&'a str>,
265        }
266
267        let url = self.client.build_url("/v4/institutional-holdings-list");
268        self.client
269            .get_with_query(
270                &url,
271                &Query {
272                    apikey: self.client.api_key(),
273                    date,
274                },
275            )
276            .await
277    }
278
279    /// Get sample of institutional holdings
280    ///
281    /// # Arguments  
282    /// * `date` - Filing date (optional)
283    /// * `limit` - Maximum number of records to return
284    pub async fn get_bulk_institutional_holdings_sample(
285        &self,
286        date: Option<&str>,
287        limit: usize,
288    ) -> Result<Vec<BulkInstitutionalHolding>> {
289        let all_holdings = self.get_bulk_institutional_holdings(date).await?;
290        Ok(all_holdings.into_iter().take(limit).collect())
291    }
292
293    /// Get bulk earnings estimates
294    ///
295    /// # Arguments
296    /// * `period` - "annual" or "quarter"
297    ///
298    /// # Example
299    /// ```no_run
300    /// # use fmp_rs::FmpClient;
301    /// # #[tokio::main]
302    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
303    /// let client = FmpClient::builder().api_key("your_api_key").build()?;
304    /// let bulk = client.bulk();
305    /// let estimates = bulk.get_bulk_earnings_estimates_sample("quarter", 20).await?;
306    /// # Ok(())
307    /// # }
308    /// ```
309    pub async fn get_bulk_earnings_estimates(
310        &self,
311        period: &str,
312    ) -> Result<Vec<BulkEarningsEstimate>> {
313        #[derive(Serialize)]
314        struct Query<'a> {
315            period: &'a str,
316            apikey: &'a str,
317        }
318
319        let url = self.client.build_url("/v4/earnings-estimates");
320        self.client
321            .get_with_query(
322                &url,
323                &Query {
324                    period,
325                    apikey: self.client.api_key(),
326                },
327            )
328            .await
329    }
330
331    /// Get sample of earnings estimates
332    ///
333    /// # Arguments
334    /// * `period` - "annual" or "quarter"  
335    /// * `limit` - Maximum number of records to return
336    pub async fn get_bulk_earnings_estimates_sample(
337        &self,
338        period: &str,
339        limit: usize,
340    ) -> Result<Vec<BulkEarningsEstimate>> {
341        let all_estimates = self.get_bulk_earnings_estimates(period).await?;
342        Ok(all_estimates.into_iter().take(limit).collect())
343    }
344
345    /// Get bulk dataset information/metadata
346    ///
347    /// Returns metadata about available bulk datasets including size,
348    /// last update time, and download URLs.
349    ///
350    /// # Example
351    /// ```no_run
352    /// # use fmp_rs::FmpClient;
353    /// # #[tokio::main]
354    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
355    /// let client = FmpClient::builder().api_key("your_api_key").build()?;
356    /// let bulk = client.bulk();
357    /// let info = bulk.get_bulk_data_info().await?;
358    ///
359    /// for dataset in info {
360    ///     println!("Dataset: {:?}, Size: {:?} MB",
361    ///         dataset.dataset,
362    ///         dataset.file_size.map(|s| s / 1024 / 1024));
363    /// }
364    /// # Ok(())
365    /// # }
366    /// ```
367    pub async fn get_bulk_data_info(&self) -> Result<Vec<BulkDataInfo>> {
368        #[derive(Serialize)]
369        struct Query<'a> {
370            apikey: &'a str,
371        }
372
373        let url = self.client.build_url("/v4/bulk-data-info");
374        self.client
375            .get_with_query(
376                &url,
377                &Query {
378                    apikey: self.client.api_key(),
379                },
380            )
381            .await
382    }
383
384    /// Get historical prices metadata for bulk download planning
385    ///
386    /// Returns information about available historical price datasets
387    /// to help plan bulk downloads efficiently.
388    ///
389    /// # Arguments
390    /// * `exchange` - Exchange identifier (optional, e.g., "NYSE", "NASDAQ")
391    ///
392    /// # Example
393    /// ```no_run
394    /// # use fmp_rs::FmpClient;
395    /// # #[tokio::main]
396    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
397    /// let client = FmpClient::builder().api_key("your_api_key").build()?;
398    /// let bulk = client.bulk();
399    /// let meta = bulk.get_historical_prices_metadata(Some("NYSE")).await?;
400    ///
401    /// for info in meta {
402    ///     println!("NYSE Historical Data: {} symbols, ~{} MB",
403    ///         info.symbols_count.unwrap_or(0),
404    ///         info.estimated_size_mb.unwrap_or(0.0));
405    /// }
406    /// # Ok(())
407    /// # }
408    /// ```
409    pub async fn get_historical_prices_metadata(
410        &self,
411        exchange: Option<&str>,
412    ) -> Result<Vec<BulkHistoricalPricesMeta>> {
413        #[derive(Serialize)]
414        struct Query<'a> {
415            apikey: &'a str,
416            #[serde(skip_serializing_if = "Option::is_none")]
417            exchange: Option<&'a str>,
418        }
419
420        let url = self.client.build_url("/v4/bulk-historical-metadata");
421        self.client
422            .get_with_query(
423                &url,
424                &Query {
425                    apikey: self.client.api_key(),
426                    exchange,
427                },
428            )
429            .await
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    fn create_test_client() -> FmpClient {
438        FmpClient::builder().api_key("test_key").build().unwrap()
439    }
440
441    fn get_test_bulk() -> Bulk {
442        Bulk::new(create_test_client())
443    }
444
445    #[test]
446    fn test_new() {
447        let client = create_test_client();
448        let _bulk = Bulk::new(client);
449    }
450
451    // Lightweight tests - only testing samples, not full datasets
452
453    #[tokio::test]
454    #[ignore] // Requires API key
455    async fn test_get_bulk_prices_sample() {
456        let bulk = get_test_bulk();
457        let result = bulk.get_bulk_prices_sample(5).await;
458        assert!(result.is_ok());
459
460        let prices = result.unwrap();
461        assert!(prices.len() <= 5);
462        if !prices.is_empty() {
463            let price = &prices[0];
464            assert!(price.symbol.is_some() || price.price.is_some());
465        }
466    }
467
468    #[tokio::test]
469    #[ignore] // Requires API key  
470    async fn test_get_bulk_financials_sample() {
471        let bulk = get_test_bulk();
472        let result = bulk
473            .get_bulk_financials_sample("annual", Some(2023), 3)
474            .await;
475        assert!(result.is_ok());
476
477        let statements = result.unwrap();
478        assert!(statements.len() <= 3);
479    }
480
481    #[tokio::test]
482    #[ignore] // Requires API key
483    async fn test_get_bulk_etf_holdings_sample() {
484        let bulk = get_test_bulk();
485        let result = bulk.get_bulk_etf_holdings_sample(5).await;
486        assert!(result.is_ok());
487
488        let holdings = result.unwrap();
489        assert!(holdings.len() <= 5);
490    }
491
492    #[tokio::test]
493    #[ignore] // Requires API key
494    async fn test_get_bulk_insider_trades_sample() {
495        let bulk = get_test_bulk();
496        let result = bulk.get_bulk_insider_trades_sample(3).await;
497        assert!(result.is_ok());
498
499        let trades = result.unwrap();
500        assert!(trades.len() <= 3);
501    }
502
503    #[tokio::test]
504    #[ignore] // Requires API key
505    async fn test_get_bulk_institutional_holdings_sample() {
506        let bulk = get_test_bulk();
507        let result = bulk.get_bulk_institutional_holdings_sample(None, 3).await;
508        assert!(result.is_ok());
509
510        let holdings = result.unwrap();
511        assert!(holdings.len() <= 3);
512    }
513
514    #[tokio::test]
515    #[ignore] // Requires API key
516    async fn test_get_bulk_earnings_estimates_sample() {
517        let bulk = get_test_bulk();
518        let result = bulk.get_bulk_earnings_estimates_sample("quarter", 3).await;
519        assert!(result.is_ok());
520
521        let estimates = result.unwrap();
522        assert!(estimates.len() <= 3);
523    }
524
525    #[tokio::test]
526    #[ignore] // Requires API key
527    async fn test_get_bulk_data_info() {
528        let bulk = get_test_bulk();
529        let result = bulk.get_bulk_data_info().await;
530        assert!(result.is_ok());
531
532        let info = result.unwrap();
533        // Info endpoint should be lightweight
534        for dataset in &info {
535            if let Some(name) = &dataset.dataset {
536                println!("Available dataset: {}", name);
537            }
538        }
539    }
540
541    #[tokio::test]
542    #[ignore] // Requires API key
543    async fn test_get_historical_prices_metadata() {
544        let bulk = get_test_bulk();
545        let result = bulk.get_historical_prices_metadata(Some("NYSE")).await;
546        assert!(result.is_ok());
547
548        let meta = result.unwrap();
549        // Metadata should be lightweight
550        for info in &meta {
551            if let Some(count) = info.symbols_count {
552                assert!(count > 0);
553            }
554        }
555    }
556
557    // Missing endpoint tests - these test endpoints not previously covered
558    #[tokio::test]
559    #[ignore] // Requires API key - WARNING: This downloads large datasets
560    async fn test_get_bulk_stock_prices() {
561        let bulk = get_test_bulk();
562        // Only test if user explicitly wants full bulk download
563        let result = bulk.get_bulk_stock_prices().await;
564        // This should work but will be very large, so we just test the call succeeds
565        match result {
566            Ok(prices) => {
567                println!("Successfully retrieved {} stock prices", prices.len());
568                // Validate structure if any data returned
569                if !prices.is_empty() {
570                    assert!(prices[0].symbol.is_some() || prices[0].price.is_some());
571                }
572            }
573            Err(e) => {
574                // May fail due to size limits or API restrictions
575                println!("Bulk download failed (expected for large datasets): {}", e);
576            }
577        }
578    }
579
580    #[tokio::test]
581    #[ignore] // Requires API key - WARNING: This downloads large datasets
582    async fn test_get_bulk_financial_statements() {
583        let bulk = get_test_bulk();
584        let result = bulk
585            .get_bulk_financial_statements("annual", Some(2023))
586            .await;
587        match result {
588            Ok(statements) => {
589                println!(
590                    "Successfully retrieved {} financial statements",
591                    statements.len()
592                );
593                if !statements.is_empty() {
594                    assert!(statements[0].symbol.is_some());
595                }
596            }
597            Err(e) => {
598                println!("Bulk financial download failed (expected): {}", e);
599            }
600        }
601    }
602
603    #[tokio::test]
604    #[ignore] // Requires API key - WARNING: This downloads large datasets  
605    async fn test_get_bulk_etf_holdings() {
606        let bulk = get_test_bulk();
607        let result = bulk.get_bulk_etf_holdings().await;
608        match result {
609            Ok(holdings) => {
610                println!("Successfully retrieved {} ETF holdings", holdings.len());
611                if !holdings.is_empty() {
612                    assert!(holdings[0].etf_symbol.is_some() || holdings[0].asset_symbol.is_some());
613                }
614            }
615            Err(e) => {
616                println!("Bulk ETF holdings download failed (expected): {}", e);
617            }
618        }
619    }
620
621    #[tokio::test]
622    #[ignore] // Requires API key - WARNING: This downloads large datasets
623    async fn test_get_bulk_insider_trades() {
624        let bulk = get_test_bulk();
625        let result = bulk.get_bulk_insider_trades().await;
626        match result {
627            Ok(trades) => {
628                println!("Successfully retrieved {} insider trades", trades.len());
629                if !trades.is_empty() {
630                    assert!(trades[0].symbol.is_some());
631                }
632            }
633            Err(e) => {
634                println!("Bulk insider trades download failed (expected): {}", e);
635            }
636        }
637    }
638
639    #[tokio::test]
640    #[ignore] // Requires API key - WARNING: This downloads large datasets
641    async fn test_get_bulk_institutional_holdings() {
642        let bulk = get_test_bulk();
643        let result = bulk.get_bulk_institutional_holdings(None).await;
644        match result {
645            Ok(holdings) => {
646                println!(
647                    "Successfully retrieved {} institutional holdings",
648                    holdings.len()
649                );
650                if !holdings.is_empty() {
651                    assert!(holdings[0].ticker_symbol.is_some());
652                }
653            }
654            Err(e) => {
655                println!(
656                    "Bulk institutional holdings download failed (expected): {}",
657                    e
658                );
659            }
660        }
661    }
662
663    #[tokio::test]
664    #[ignore] // Requires API key - WARNING: This downloads large datasets
665    async fn test_get_bulk_earnings_estimates() {
666        let bulk = get_test_bulk();
667        let result = bulk.get_bulk_earnings_estimates("quarter").await;
668        match result {
669            Ok(estimates) => {
670                println!(
671                    "Successfully retrieved {} earnings estimates",
672                    estimates.len()
673                );
674                if !estimates.is_empty() {
675                    assert!(estimates[0].symbol.is_some());
676                }
677            }
678            Err(e) => {
679                println!("Bulk earnings estimates download failed (expected): {}", e);
680            }
681        }
682    }
683
684    // Edge case tests for sample functions
685    #[tokio::test]
686    #[ignore] // Requires API key
687    async fn test_get_bulk_prices_sample_edge_cases() {
688        let bulk = get_test_bulk();
689
690        // Test with zero limit
691        let result = bulk.get_bulk_prices_sample(0).await;
692        match result {
693            Ok(prices) => assert!(prices.is_empty()),
694            Err(_) => {} // May return error for zero limit
695        }
696
697        // Test with very large limit
698        let result = bulk.get_bulk_prices_sample(1000000).await;
699        match result {
700            Ok(prices) => {
701                // Should be capped by API or return reasonable amount
702                assert!(prices.len() <= 1000000);
703            }
704            Err(_) => {} // May return error for excessive limit
705        }
706    }
707
708    #[tokio::test]
709    #[ignore] // Requires API key
710    async fn test_get_bulk_financials_sample_edge_cases() {
711        let bulk = get_test_bulk();
712
713        // Test invalid period
714        let result = bulk
715            .get_bulk_financials_sample("invalid_period", Some(2023), 3)
716            .await;
717        match result {
718            Ok(statements) => assert!(statements.is_empty()),
719            Err(_) => {} // Should return error for invalid period
720        }
721
722        // Test future year
723        let result = bulk
724            .get_bulk_financials_sample("annual", Some(2030), 3)
725            .await;
726        match result {
727            Ok(statements) => assert!(statements.is_empty()), // Future data shouldn't exist
728            Err(_) => {}                                      // May return error for future dates
729        }
730
731        // Test very old year
732        let result = bulk
733            .get_bulk_financials_sample("annual", Some(1900), 3)
734            .await;
735        match result {
736            Ok(statements) => assert!(statements.is_empty()),
737            Err(_) => {} // May return error for very old dates
738        }
739    }
740
741    #[tokio::test]
742    #[ignore] // Requires API key
743    async fn test_get_historical_prices_metadata_edge_cases() {
744        let bulk = get_test_bulk();
745
746        // Test invalid exchange
747        let result = bulk
748            .get_historical_prices_metadata(Some("INVALID_EXCHANGE"))
749            .await;
750        match result {
751            Ok(meta) => assert!(meta.is_empty()),
752            Err(_) => {} // May return error for invalid exchange
753        }
754
755        // Test without exchange filter
756        let result = bulk.get_historical_prices_metadata(None).await;
757        assert!(result.is_ok()); // Should work without filter
758    }
759
760    #[tokio::test]
761    #[ignore] // Requires API key
762    async fn test_bulk_sample_data_validation() {
763        let bulk = get_test_bulk();
764        let result = bulk.get_bulk_prices_sample(3).await;
765        assert!(result.is_ok());
766
767        let prices = result.unwrap();
768        for price in &prices {
769            // Validate data structure
770            if let Some(ref symbol) = price.symbol {
771                assert!(!symbol.is_empty());
772                assert!(symbol.len() <= 10); // Reasonable symbol length
773            }
774            if let Some(price_val) = price.price {
775                assert!(price_val > 0.0); // Price should be positive
776            }
777            if let Some(volume) = price.volume {
778                assert!(volume >= 0); // Volume should be non-negative
779            }
780        }
781    }
782
783    // Model serialization tests
784    #[test]
785    fn test_bulk_stock_price_serialization() {
786        let price = BulkStockPrice {
787            symbol: Some("AAPL".to_string()),
788            name: Some("Apple Inc".to_string()),
789            price: Some(150.25),
790            change: Some(2.50),
791            changes_percentage: Some(1.69),
792            volume: Some(50000000),
793            market_cap: Some(2500000000000.0),
794            pe: Some(28.5),
795            exchange: Some("NASDAQ".to_string()),
796            day_low: Some(148.50),
797            day_high: Some(151.00),
798            year_low: Some(124.17),
799            year_high: Some(182.94),
800            avg_volume: Some(60000000),
801            open: Some(149.50),
802            previous_close: Some(147.75),
803            eps: Some(5.28),
804            shares_outstanding: Some(16500000000),
805            timestamp: Some(1640995200),
806        };
807
808        let json = serde_json::to_string(&price).unwrap();
809        let deserialized: BulkStockPrice = serde_json::from_str(&json).unwrap();
810        assert_eq!(deserialized.symbol, price.symbol);
811        assert_eq!(deserialized.price, price.price);
812    }
813
814    #[test]
815    fn test_bulk_data_info_serialization() {
816        let info = BulkDataInfo {
817            dataset: Some("stock_prices".to_string()),
818            last_updated: Some("2024-01-15T10:30:00Z".to_string()),
819            file_size: Some(104857600), // 100MB
820            record_count: Some(1000000),
821            download_url: Some("https://api.fmp.com/bulk/stock_prices.csv".to_string()),
822            format: Some("CSV".to_string()),
823            compression: Some("gzip".to_string()),
824            schema_version: Some("v1.2".to_string()),
825        };
826
827        let json = serde_json::to_string(&info).unwrap();
828        let deserialized: BulkDataInfo = serde_json::from_str(&json).unwrap();
829        assert_eq!(deserialized.dataset, info.dataset);
830        assert_eq!(deserialized.file_size, info.file_size);
831    }
832
833    // Additional edge case tests for missing endpoints
834    #[tokio::test]
835    #[ignore = "requires FMP API key"]
836    async fn test_get_bulk_income_statements() {
837        let client = FmpClient::new().unwrap();
838        let result = client.bulk().get_bulk_income_statements().await;
839        assert!(result.is_ok());
840        let statements = result.unwrap();
841        if !statements.is_empty() {
842            assert!(statements[0].symbol.is_some());
843            assert!(statements[0].revenue.is_some() || statements[0].net_income.is_some());
844        }
845    }
846
847    #[tokio::test]
848    #[ignore = "requires FMP API key"]
849    async fn test_get_bulk_income_statements_sample() {
850        let client = FmpClient::new().unwrap();
851        let result = client.bulk().get_bulk_income_statements_sample().await;
852        assert!(result.is_ok());
853        let statements = result.unwrap();
854        assert!(statements.len() <= 100); // Sample should be limited
855        if !statements.is_empty() {
856            assert!(statements[0].symbol.is_some());
857        }
858    }
859
860    #[tokio::test]
861    #[ignore = "requires FMP API key"]
862    async fn test_get_bulk_balance_sheets() {
863        let client = FmpClient::new().unwrap();
864        let result = client.bulk().get_bulk_balance_sheets().await;
865        assert!(result.is_ok());
866        let sheets = result.unwrap();
867        if !sheets.is_empty() {
868            assert!(sheets[0].symbol.is_some());
869            assert!(sheets[0].total_assets.is_some() || sheets[0].total_liabilities.is_some());
870        }
871    }
872
873    #[tokio::test]
874    #[ignore = "requires FMP API key"]
875    async fn test_get_bulk_balance_sheets_sample() {
876        let client = FmpClient::new().unwrap();
877        let result = client.bulk().get_bulk_balance_sheets_sample().await;
878        assert!(result.is_ok());
879        let sheets = result.unwrap();
880        assert!(sheets.len() <= 100); // Sample should be limited
881    }
882
883    #[tokio::test]
884    #[ignore = "requires FMP API key"]
885    async fn test_get_bulk_cash_flow_statements() {
886        let client = FmpClient::new().unwrap();
887        let result = client.bulk().get_bulk_cash_flow_statements().await;
888        assert!(result.is_ok());
889        let cash_flows = result.unwrap();
890        if !cash_flows.is_empty() {
891            assert!(cash_flows[0].symbol.is_some());
892            assert!(
893                cash_flows[0].operating_cash_flow.is_some()
894                    || cash_flows[0].free_cash_flow.is_some()
895            );
896        }
897    }
898
899    #[tokio::test]
900    #[ignore = "requires FMP API key"]
901    async fn test_get_bulk_cash_flow_statements_sample() {
902        let client = FmpClient::new().unwrap();
903        let result = client.bulk().get_bulk_cash_flow_statements_sample().await;
904        assert!(result.is_ok());
905        let cash_flows = result.unwrap();
906        assert!(cash_flows.len() <= 100); // Sample should be limited
907    }
908
909    #[tokio::test]
910    #[ignore = "requires FMP API key"]
911    async fn test_get_bulk_etf_holdings() {
912        let client = FmpClient::new().unwrap();
913        let result = client.bulk().get_bulk_etf_holdings().await;
914        assert!(result.is_ok());
915        let holdings = result.unwrap();
916        if !holdings.is_empty() {
917            assert!(holdings[0].etf_symbol.is_some());
918            assert!(holdings[0].holding_symbol.is_some());
919        }
920    }
921
922    #[tokio::test]
923    #[ignore = "requires FMP API key"]
924    async fn test_get_bulk_etf_holdings_sample() {
925        let client = FmpClient::new().unwrap();
926        let result = client.bulk().get_bulk_etf_holdings_sample().await;
927        assert!(result.is_ok());
928        let holdings = result.unwrap();
929        assert!(holdings.len() <= 100); // Sample should be limited
930    }
931
932    #[tokio::test]
933    #[ignore = "requires FMP API key"]
934    async fn test_get_bulk_data_info() {
935        let client = FmpClient::new().unwrap();
936        let result = client.bulk().get_bulk_data_info().await;
937        assert!(result.is_ok());
938        let info = result.unwrap();
939        if !info.is_empty() {
940            assert!(info[0].dataset.is_some());
941            assert!(info[0].file_size.is_some() || info[0].record_count.is_some());
942        }
943    }
944
945    #[tokio::test]
946    #[ignore = "requires FMP API key"]
947    async fn test_get_historical_prices_metadata() {
948        let client = FmpClient::new().unwrap();
949        let result = client
950            .bulk()
951            .get_historical_prices_metadata(Some("NYSE"))
952            .await;
953        assert!(result.is_ok());
954        let metadata = result.unwrap();
955        if !metadata.is_empty() {
956            assert!(metadata[0].exchange.is_some());
957        }
958    }
959
960    #[tokio::test]
961    #[ignore = "requires FMP API key"]
962    async fn test_get_historical_prices_metadata_no_exchange() {
963        let client = FmpClient::new().unwrap();
964        let result = client.bulk().get_historical_prices_metadata(None).await;
965        assert!(result.is_ok());
966        // Should return metadata for all exchanges
967    }
968
969    // Error handling and edge cases
970    #[tokio::test]
971    #[ignore = "requires FMP API key"]
972    async fn test_bulk_endpoints_error_handling() {
973        let client = FmpClient::builder()
974            .api_key("invalid_key_12345")
975            .build()
976            .unwrap();
977
978        let result1 = client.bulk().get_bulk_stock_prices().await;
979        let result2 = client.bulk().get_bulk_income_statements().await;
980        let result3 = client.bulk().get_bulk_data_info().await;
981
982        // All should return errors for invalid API key
983        assert!(result1.is_err());
984        assert!(result2.is_err());
985        assert!(result3.is_err());
986    }
987
988    #[tokio::test]
989    #[ignore = "requires FMP API key"]
990    async fn test_sample_vs_full_data_consistency() {
991        let client = FmpClient::new().unwrap();
992
993        // Compare sample vs full data structure
994        let sample_result = client.bulk().get_bulk_stock_prices_sample().await;
995        let full_result = client.bulk().get_bulk_stock_prices().await;
996
997        assert!(sample_result.is_ok());
998        assert!(full_result.is_ok());
999
1000        let sample_data = sample_result.unwrap();
1001        let full_data = full_result.unwrap();
1002
1003        if !sample_data.is_empty() && !full_data.is_empty() {
1004            // Sample should have same structure as full data, just fewer records
1005            assert!(sample_data.len() <= full_data.len());
1006
1007            // Both should have similar field patterns
1008            if sample_data[0].symbol.is_some() {
1009                assert!(full_data.iter().any(|p| p.symbol.is_some()));
1010            }
1011        }
1012    }
1013}