//! ProductView delegate trait and supporting types for Polkadot product host embedding.
//!
//! This crate defines the contract between the host-api message processor and
//! the platform-specific host layer (iOS, Android, desktop, WASM). It is
//! intentionally free of host-api internals so it can be implemented across
//! all target platforms without pulling in SCALE codecs or smoldot.
//!
//! # Usage
//!
//! 1. Implement [`ProductViewDelegate`] for your platform's view controller.
//! 2. After calling `HostApi::handle_message`, map the `HostApiOutcome`
//!    fields to the corresponding delegate method.
//! 3. Use [`ProductLoadState`] to drive loading UI and [`ProductAssets`]
//!    to serve the verified bundle into the WebView.

pub mod delegate;
pub mod error;
pub mod grant_store;
pub mod manifest;
pub mod mime;
pub mod permissions;
pub mod preflight;
pub mod security;
pub mod serving;
pub mod state;

pub use delegate::{ChainRpcRequest, ProductViewDelegate};
pub use error::ProductError;
pub use grant_store::{InMemorySandboxGrantStore, SandboxGrantStore, SandboxGrantStoreError};
pub use manifest::{
    ConsentChainTarget, ConsentSandboxRequest, HostChainRequirements, HostChainTarget,
    HostManifest, HostSandboxRequest, HostSandboxRequirements, ManifestError,
    PreparedProductBundle, PresentationConsentSummary, ProductBundleError, ProductConsentSummary,
    ProductManifest, ProductMetadata, ProductModality, ProductSource, PublisherMetadata,
    SandboxRequestKind, VerifiedConsentSummary,
};
pub use permissions::{
    EffectiveSandboxPermissions, GrantedSandboxRequest, SandboxGrant, SandboxPermissionResolution,
};
pub use preflight::{
    ConsentDenyBehavior, ProductConsentDecision, ProductPreflightContext,
    ProductPreflightDenyReason, ProductPreflightError, ProductPreflightOutcome,
    ProductPreflightPrompt,
};
pub use security::{csp_for_scheme, inject_into_head};
pub use serving::serve_asset;
pub use state::{ProductAssets, ProductLoadState};

#[cfg(test)]
mod tests {
    use super::*;

    /// Mock delegate that records calls for verification.
    struct MockDelegate {
        last_product_id: std::cell::RefCell<String>,
    }

    impl MockDelegate {
        fn new() -> Self {
            Self {
                last_product_id: std::cell::RefCell::new(String::new()),
            }
        }
    }

    impl ProductViewDelegate for MockDelegate {
        fn send_response(&self, product_id: &str, _bytes: Vec<u8>) -> Result<(), ProductError> {
            *self.last_product_id.borrow_mut() = product_id.to_string();
            Ok(())
        }

        fn needs_sign(
            &self,
            product_id: &str,
            _request_id: &str,
            _request_tag: u8,
            _public_key: Vec<u8>,
            _payload: Vec<u8>,
        ) -> Result<(), ProductError> {
            *self.last_product_id.borrow_mut() = product_id.to_string();
            Ok(())
        }

        fn needs_chain_query(
            &self,
            product_id: &str,
            _request_id: &str,
            _method: &str,
            _params_json: &str,
        ) -> Result<(), ProductError> {
            *self.last_product_id.borrow_mut() = product_id.to_string();
            Ok(())
        }

        fn needs_chain_subscription(
            &self,
            product_id: &str,
            _request_id: &str,
            _method: &str,
            _params_json: &str,
        ) -> Result<(), ProductError> {
            *self.last_product_id.borrow_mut() = product_id.to_string();
            Ok(())
        }

        fn needs_chain_follow(
            &self,
            product_id: &str,
            _request_id: &str,
            _genesis_hash: Vec<u8>,
            _with_runtime: bool,
        ) -> Result<(), ProductError> {
            *self.last_product_id.borrow_mut() = product_id.to_string();
            Ok(())
        }

        fn needs_chain_rpc(
            &self,
            product_id: &str,
            _request_id: &str,
            _req: ChainRpcRequest,
        ) -> Result<(), ProductError> {
            *self.last_product_id.borrow_mut() = product_id.to_string();
            Ok(())
        }

        fn needs_navigate(
            &self,
            product_id: &str,
            _request_id: &str,
            _url: &str,
        ) -> Result<(), ProductError> {
            *self.last_product_id.borrow_mut() = product_id.to_string();
            Ok(())
        }
    }

    #[test]
    fn test_mock_delegate_send_response() {
        let d = MockDelegate::new();
        d.send_response("app.dot", vec![1, 2, 3]).unwrap();
        assert_eq!(*d.last_product_id.borrow(), "app.dot");
    }

    #[test]
    fn test_mock_delegate_needs_sign() {
        let d = MockDelegate::new();
        d.needs_sign("app.dot", "req-1", 42, vec![0; 32], vec![0; 64])
            .unwrap();
        assert_eq!(*d.last_product_id.borrow(), "app.dot");
    }

    #[test]
    fn test_mock_delegate_needs_chain_rpc() {
        let d = MockDelegate::new();
        let req = ChainRpcRequest {
            request_tag: 1,
            genesis_hash: vec![0; 32],
            method: "chainHead_v1_header".into(),
            params_json: "[]".into(),
            follow_sub_id: Some("sub-1".into()),
        };
        d.needs_chain_rpc("app.dot", "req-2", req.clone()).unwrap();
        assert_eq!(*d.last_product_id.borrow(), "app.dot");
        // Verify Clone works
        assert_eq!(req.method, "chainHead_v1_header");
    }

    #[test]
    fn test_mock_delegate_needs_navigate() {
        let d = MockDelegate::new();
        d.needs_navigate("app.dot", "req-3", "https://polkadot.network")
            .unwrap();
        assert_eq!(*d.last_product_id.borrow(), "app.dot");
    }

    #[test]
    fn test_delegate_returns_error() {
        struct FailingDelegate;
        impl ProductViewDelegate for FailingDelegate {
            fn send_response(&self, _: &str, _: Vec<u8>) -> Result<(), ProductError> {
                Err(ProductError::UnknownProduct("nope".into()))
            }
            fn needs_sign(
                &self,
                _: &str,
                _: &str,
                _: u8,
                _: Vec<u8>,
                _: Vec<u8>,
            ) -> Result<(), ProductError> {
                Err(ProductError::SignFailed("locked".into()))
            }
            fn needs_chain_query(
                &self,
                _: &str,
                _: &str,
                _: &str,
                _: &str,
            ) -> Result<(), ProductError> {
                Err(ProductError::ChainRpcFailed("offline".into()))
            }
            fn needs_chain_subscription(
                &self,
                _: &str,
                _: &str,
                _: &str,
                _: &str,
            ) -> Result<(), ProductError> {
                Err(ProductError::ChainSubscriptionFailed("no chain".into()))
            }
            fn needs_chain_follow(
                &self,
                _: &str,
                _: &str,
                _: Vec<u8>,
                _: bool,
            ) -> Result<(), ProductError> {
                Err(ProductError::ChainFollowFailed("gone".into()))
            }
            fn needs_chain_rpc(
                &self,
                _: &str,
                _: &str,
                _: ChainRpcRequest,
            ) -> Result<(), ProductError> {
                Err(ProductError::ChainRpcFailed("timeout".into()))
            }
            fn needs_navigate(&self, _: &str, _: &str, _: &str) -> Result<(), ProductError> {
                Err(ProductError::NavigateRejected("blocked".into()))
            }
        }

        let d = FailingDelegate;
        assert!(d.send_response("x", vec![]).is_err());
        assert!(d.needs_sign("x", "r", 0, vec![], vec![]).is_err());
        assert!(d.needs_chain_query("x", "r", "m", "[]").is_err());
        assert!(d.needs_chain_subscription("x", "r", "m", "[]").is_err());
        assert!(d.needs_chain_follow("x", "r", vec![], false).is_err());
        assert!(d
            .needs_chain_rpc(
                "x",
                "r",
                ChainRpcRequest {
                    request_tag: 0,
                    genesis_hash: vec![],
                    method: String::new(),
                    params_json: String::new(),
                    follow_sub_id: None,
                }
            )
            .is_err());
        assert!(d.needs_navigate("x", "r", "url").is_err());
    }
}
