//! TWSE (Taiwan Stock Exchange) endpoints. No API key required.
//!
//! Unlike the equity providers, TWSE handlers return bare `Vec<T>` / `T`
//! (NOT wrapped in `FinanceResult`), so these methods do too.
//!
//! The two OTC/TPEx endpoints hit a Cloudflare-protected host and return 503
//! upstream; they are wired for completeness.

use motosan_finance_twse::{
    ForeignOwnership, InstitutionalFlow, MarginTrading, ShareholderDistribution, StockDayRecord,
};
use serde::Serialize;

use crate::client::FinProxy;
use crate::error::SdkError;
// The shareholder endpoint is symbol-keyed; reuse the shared equity param.
pub use crate::equity::SymbolParams;

/// `?date=YYYY-MM-DD` — date-keyed TWSE datasets.
#[derive(Debug, Clone, Serialize)]
pub struct DateParams {
    pub date: String,
}

impl DateParams {
    pub fn new(date: impl Into<String>) -> Self {
        Self { date: date.into() }
    }
}

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

impl FinProxy {
    /// Access the TWSE (Taiwan Stock Exchange) endpoints.
    pub fn twse(&self) -> Twse<'_> {
        Twse { client: self }
    }
}

impl Twse<'_> {
    /// GET /v1/twse/stock-day-all — every listed stock's daily OHLCV.
    pub async fn stock_day_all(
        &self,
        params: &DateParams,
    ) -> Result<Vec<StockDayRecord>, SdkError> {
        self.client.get("/v1/twse/stock-day-all", params).await
    }

    /// GET /v1/twse/institutional — three-institution net flows.
    pub async fn institutional(
        &self,
        params: &DateParams,
    ) -> Result<Vec<InstitutionalFlow>, SdkError> {
        self.client.get("/v1/twse/institutional", params).await
    }

    /// GET /v1/twse/margin — margin trading balances.
    pub async fn margin(&self, params: &DateParams) -> Result<Vec<MarginTrading>, SdkError> {
        self.client.get("/v1/twse/margin", params).await
    }

    /// GET /v1/twse/otc-institutional — OTC/TPEx institutional flows (503 upstream).
    pub async fn otc_institutional(
        &self,
        params: &DateParams,
    ) -> Result<Vec<InstitutionalFlow>, SdkError> {
        self.client.get("/v1/twse/otc-institutional", params).await
    }

    /// GET /v1/twse/otc-margin — OTC/TPEx margin balances (503 upstream).
    pub async fn otc_margin(&self, params: &DateParams) -> Result<Vec<MarginTrading>, SdkError> {
        self.client.get("/v1/twse/otc-margin", params).await
    }

    /// GET /v1/twse/foreign-ownership — foreign holding ratios.
    pub async fn foreign_ownership(
        &self,
        params: &DateParams,
    ) -> Result<Vec<ForeignOwnership>, SdkError> {
        self.client.get("/v1/twse/foreign-ownership", params).await
    }

    /// GET /v1/twse/shareholder — TDCC shareholder distribution for one symbol.
    pub async fn shareholder(
        &self,
        params: &SymbolParams,
    ) -> Result<ShareholderDistribution, SdkError> {
        self.client.get("/v1/twse/shareholder", params).await
    }
}

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

    #[tokio::test]
    async fn stock_day_all_hits_path_with_bare_vec() {
        let server = MockServer::start().await;
        // TWSE returns a BARE array in `data` (no FinanceResult). Empty array keeps
        // StockDayRecord's many required fields out of the fixture; this verifies the
        // path, the date query encoding, and that the SDK unwraps to Vec<_> directly.
        let body = r#"{"success":true,"data":[],"meta":{}}"#;
        Mock::given(method("GET"))
            .and(path("/v1/twse/stock-day-all"))
            .and(query_param("date", "2024-01-02"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body))
            .mount(&server)
            .await;

        let client = FinProxy::new(server.uri()).unwrap();
        let rows = client.twse().stock_day_all(&DateParams::new("2024-01-02")).await.unwrap();
        assert!(rows.is_empty());
    }

    #[tokio::test]
    async fn shareholder_parses_single_object() {
        let server = MockServer::start().await;
        // shareholder returns a single OBJECT (not an array). Provide a valid
        // ShareholderDistribution; `tiers` may be empty.
        let body = r#"{"success":true,"data":{"symbol":"2330","date":"2024-01-02",
            "tiers":[],"large_holder_pct":75.5,"retail_pct":24.5},"meta":{}}"#;
        Mock::given(method("GET"))
            .and(path("/v1/twse/shareholder"))
            .and(query_param("symbol", "2330"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body))
            .mount(&server)
            .await;

        let client = FinProxy::new(server.uri()).unwrap();
        let dist = client.twse().shareholder(&SymbolParams::new("2330")).await.unwrap();
        assert_eq!(dist.symbol, "2330");
        assert_eq!(dist.large_holder_pct, 75.5);
        assert!(dist.tiers.is_empty());
    }
}
