//! FMP (US equities + fundamentals) endpoints.

use motosan_finance::{EquityHistoricalData, EquityProfileData, EquityQuoteData, FinanceResult};
use motosan_finance_fmp::{AnalystConsensus, BalanceSheetStatement, CashFlowStatement, EarningsData, IncomeStatement};
use serde::Serialize;

use crate::client::FinProxy;
use crate::error::SdkError;
// Equity params live in the shared module; re-exported here so `fmp::SymbolParams`
// and `fmp::HistoricalParams` remain valid paths for existing callers.
pub use crate::equity::{HistoricalParams, SymbolParams};

/// Reporting period for financial statements (`?period=quarter|annual`).
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Period {
    Quarter,
    Annual,
}

/// `?symbol=&period=&limit=` for income/balance/cash-flow statements.
#[derive(Debug, Clone, Serialize, Default)]
pub struct StatementParams {
    pub symbol: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub period: Option<Period>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<u32>,
}

impl StatementParams {
    pub fn new(symbol: impl Into<String>) -> Self {
        Self { symbol: symbol.into(), ..Default::default() }
    }
    pub fn period(mut self, p: Period) -> Self {
        self.period = Some(p);
        self
    }
    pub fn limit(mut self, n: u32) -> Self {
        self.limit = Some(n);
        self
    }
}

/// `?symbol=&start=&end=` (all optional) for the FMP earnings calendar.
#[derive(Debug, Clone, Serialize, Default)]
pub struct EarningsCalendarParams {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub symbol: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end: Option<String>,
}

impl EarningsCalendarParams {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn symbol(mut self, s: impl Into<String>) -> Self {
        self.symbol = Some(s.into());
        self
    }
    pub fn start(mut self, d: impl Into<String>) -> Self {
        self.start = Some(d.into());
        self
    }
    pub fn end(mut self, d: impl Into<String>) -> Self {
        self.end = Some(d.into());
        self
    }
}

/// FMP sub-client, obtained via [`FinProxy::fmp`].
pub struct Fmp<'a> {
    pub(crate) client: &'a FinProxy,
}

impl FinProxy {
    /// Access the FMP (US equities + fundamentals) endpoints.
    pub fn fmp(&self) -> Fmp<'_> {
        Fmp { client: self }
    }
}

impl Fmp<'_> {
    /// GET /v1/fmp/equity/price/historical
    pub async fn historical(
        &self,
        params: &HistoricalParams,
    ) -> Result<FinanceResult<Vec<EquityHistoricalData>>, SdkError> {
        self.client.get("/v1/fmp/equity/price/historical", params).await
    }

    /// GET /v1/fmp/equity/price/quote
    pub async fn quote(
        &self,
        params: &SymbolParams,
    ) -> Result<FinanceResult<Vec<EquityQuoteData>>, SdkError> {
        self.client.get("/v1/fmp/equity/price/quote", params).await
    }

    /// GET /v1/fmp/equity/profile
    pub async fn profile(
        &self,
        params: &SymbolParams,
    ) -> Result<FinanceResult<Vec<EquityProfileData>>, SdkError> {
        self.client.get("/v1/fmp/equity/profile", params).await
    }

    /// GET /v1/fmp/income-statement
    pub async fn income_statement(
        &self,
        params: &StatementParams,
    ) -> Result<FinanceResult<Vec<IncomeStatement>>, SdkError> {
        self.client.get("/v1/fmp/income-statement", params).await
    }

    /// GET /v1/fmp/balance-sheet-statement
    pub async fn balance_sheet(
        &self,
        params: &StatementParams,
    ) -> Result<FinanceResult<Vec<BalanceSheetStatement>>, SdkError> {
        self.client.get("/v1/fmp/balance-sheet-statement", params).await
    }

    /// GET /v1/fmp/cash-flow-statement
    pub async fn cash_flow(
        &self,
        params: &StatementParams,
    ) -> Result<FinanceResult<Vec<CashFlowStatement>>, SdkError> {
        self.client.get("/v1/fmp/cash-flow-statement", params).await
    }

    /// GET /v1/fmp/earnings-calendar
    pub async fn earnings_calendar(
        &self,
        params: &EarningsCalendarParams,
    ) -> Result<FinanceResult<Vec<EarningsData>>, SdkError> {
        self.client.get("/v1/fmp/earnings-calendar", params).await
    }

    /// GET /v1/fmp/analyst-grades
    pub async fn analyst_grades(
        &self,
        params: &SymbolParams,
    ) -> Result<FinanceResult<Vec<AnalystConsensus>>, SdkError> {
        self.client.get("/v1/fmp/analyst-grades", params).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use wiremock::matchers::{method, path, query_param};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    #[tokio::test]
    async fn income_statement_unwraps_envelope_and_encodes_period_and_limit() {
        let server = MockServer::start().await;
        // IMPORTANT: `IncomeStatement` has 39 REQUIRED fields (no #[serde(default)]),
        // so a partial row like {"symbol":"AAPL"} would FAIL to deserialize and this
        // test would panic on `.unwrap()` with SdkError::Decode. Keep `results`
        // EMPTY — this test verifies (a) the path, (b) period/limit query encoding
        // (the query_param matchers), and (c) envelope unwrap. Real fully-populated
        // -row deserialization is covered by `examples/smoke.rs` against a live
        // fin-proxy. Do NOT add a partial IncomeStatement row here.
        let body = r#"{"success":true,"data":{"results":[],"provider":"fmp",
            "warnings":[],"extra":{}},"meta":{}}"#;
        Mock::given(method("GET"))
            .and(path("/v1/fmp/income-statement"))
            .and(query_param("symbol", "AAPL"))
            .and(query_param("period", "annual"))
            .and(query_param("limit", "8"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body))
            .mount(&server)
            .await;

        let client = FinProxy::new(server.uri()).unwrap();
        let res = client
            .fmp()
            .income_statement(&StatementParams::new("AAPL").period(Period::Annual).limit(8))
            .await
            .unwrap();
        assert_eq!(res.provider.as_deref(), Some("fmp"));
        assert!(res.results.unwrap().is_empty());
    }

    #[tokio::test]
    async fn quote_omits_absent_optional_params() {
        let server = MockServer::start().await;
        let body = r#"{"success":true,"data":{"results":[],"provider":"fmp",
            "warnings":[],"extra":{}},"meta":{}}"#;
        Mock::given(method("GET"))
            .and(path("/v1/fmp/equity/price/quote"))
            .and(query_param("symbol", "AAPL"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body))
            .mount(&server)
            .await;

        let client = FinProxy::new(server.uri()).unwrap();
        let res = client.fmp().quote(&SymbolParams::new("AAPL")).await.unwrap();
        assert_eq!(res.provider.as_deref(), Some("fmp"));
    }

    #[tokio::test]
    async fn quote_captures_flattened_name_into_extra() {
        // Regression guard: motosan-finance-fmp >=0.7.4 forwards FMP's /quote `name`
        // as a flattened field. EquityQuoteData has no typed `name` (it's profile
        // data), so the SDK must pick it up via its `#[serde(flatten)] extra`. Pin
        // that here so a future change can't silently drop it and resurrect the
        // per-symbol profile() workaround. (A row needs only symbol+price; the rest
        // default to None.)
        let server = MockServer::start().await;
        let body = r#"{"success":true,"data":{"results":[
            {"symbol":"AAPL","price":291.58,"name":"Apple Inc."}
        ],"provider":"fmp","warnings":[],"extra":{}},"meta":{}}"#;
        Mock::given(method("GET"))
            .and(path("/v1/fmp/equity/price/quote"))
            .and(query_param("symbol", "AAPL"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body))
            .mount(&server)
            .await;

        let client = FinProxy::new(server.uri()).unwrap();
        let res = client.fmp().quote(&SymbolParams::new("AAPL")).await.unwrap();
        let rows = res.results.unwrap();
        assert_eq!(rows[0].symbol, "AAPL");
        assert_eq!(
            rows[0].extra.get("name").and_then(|v| v.as_str()),
            Some("Apple Inc.")
        );
    }

    #[tokio::test]
    async fn earnings_calendar_encodes_dates_and_unwraps() {
        let server = MockServer::start().await;
        // results kept EMPTY (EarningsData need not be populated): verifies path,
        // symbol/start/end encoding, and FinanceResult envelope unwrap.
        let body = r#"{"success":true,"data":{"results":[],"provider":"fmp",
            "warnings":[],"extra":{}},"meta":{}}"#;
        Mock::given(method("GET"))
            .and(path("/v1/fmp/earnings-calendar"))
            .and(query_param("symbol", "AAPL"))
            .and(query_param("start", "2024-01-01"))
            .and(query_param("end", "2024-03-31"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body))
            .mount(&server)
            .await;

        let client = FinProxy::new(server.uri()).unwrap();
        let res = client
            .fmp()
            .earnings_calendar(
                &EarningsCalendarParams::new()
                    .symbol("AAPL")
                    .start("2024-01-01")
                    .end("2024-03-31"),
            )
            .await
            .unwrap();
        assert_eq!(res.provider.as_deref(), Some("fmp"));
        assert!(res.results.unwrap().is_empty());
    }

    #[tokio::test]
    async fn analyst_grades_hits_path_and_unwraps() {
        let server = MockServer::start().await;
        let body = r#"{"success":true,"data":{"results":[],"provider":"fmp",
            "warnings":[],"extra":{}},"meta":{}}"#;
        Mock::given(method("GET"))
            .and(path("/v1/fmp/analyst-grades"))
            .and(query_param("symbol", "AAPL"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body))
            .mount(&server)
            .await;

        let client = FinProxy::new(server.uri()).unwrap();
        let res = client.fmp().analyst_grades(&SymbolParams::new("AAPL")).await.unwrap();
        assert_eq!(res.provider.as_deref(), Some("fmp"));
        assert!(res.results.unwrap().is_empty());
    }
}
