use async_trait::async_trait;
use indieweb::http::{Body, Client, Request};
use indieweb::mf2::from_html;
use indieweb::mf2::types::PropertyValue;
use indieweb::standards::vouch::{DomainApprover, VouchError, VouchValidationResult, VouchValidator};
use std::collections::HashSet;
use std::sync::Arc;
use url::Url;

/// Default implementation of the `VouchValidator` trait with comprehensive validation features.
///
/// This validator supports:
/// - Base validation: checks vouch URL is from an approved authority and links to sender
/// - Chain validation: recursively validates vouch chains up to a maximum depth
/// - Cycle detection: prevents infinite loops by tracking visited URLs
/// - Redirect following: handles HTTP redirects with configurable maximum hops
///
/// # Example
///
/// ```no_run
/// use indieweb::standards::vouch::{DomainApprover, AlwaysAllow};
/// use indieweb::http::reqwest::Client;
/// use url::Url;
/// use indieweb::standards::vouch::DefaultVouchValidator;
///
/// #[tokio::main]
/// async fn main() {
///     let approver = Arc::new(AlwaysAllow);
///     let http_client = Arc::new(Client::default());
///     let validator = DefaultVouchValidator::new(approver, http_client);
///
///     let vouch_url = Url::parse("https://example.com/vouch").unwrap();
///     let sender_authority = "https://sender.com";
///
///     let result = validator.validate_vouch(&vouch_url, sender_authority).await;
///     assert!(result.is_ok());
/// }
/// ```
pub struct DefaultVouchValidator {
    /// Approver for checking if an authority is approved
    approver: Arc<dyn DomainApprover>,
    /// HTTP client for fetching vouch pages
    http_client: Arc<dyn Client>,
    /// Maximum depth for vouch chain validation (default: 5)
    max_chain_depth: usize,
    /// Maximum number of redirect hops to follow (default: 3)
    max_redirect_hops: usize,
}

impl DefaultVouchValidator {
    /// Creates a new `DefaultVouchValidator` with the given approver and HTTP client.
    ///
    /// # Arguments
    ///
    /// * `approver` - A `DomainApprover` implementation for checking approved authorities
    /// * `http_client` - An HTTP client for fetching vouch pages
    ///
    /// # Example
    ///
    /// ```no_run
    /// use indieweb::standards::vouch::{DomainApprover, AlwaysAllow};
    /// use indieweb::http::reqwest::Client;
    /// use indieweb::standards::vouch::DefaultVouchValidator;
    /// use std::sync::Arc;
    ///
    /// let approver = Arc::new(AlwaysAllow);
    /// let http_client = Arc::new(Client::default());
    /// let validator = DefaultVouchValidator::new(approver, http_client);
    /// ```
    pub fn new(approver: Arc<dyn DomainApprover>, http_client: Arc<dyn Client>) -> Self {
        Self {
            approver,
            http_client,
            max_chain_depth: 5,
            max_redirect_hops: 3,
        }
    }

    /// Creates a new `DefaultVouchValidator` with custom configuration.
    ///
    /// # Arguments
    ///
    /// * `approver` - A `DomainApprover` implementation for checking approved authorities
    /// * `http_client` - An HTTP client for fetching vouch pages
    /// * `max_chain_depth` - Maximum depth for vouch chain validation
    /// * `max_redirect_hops` - Maximum number of redirect hops to follow
    ///
    /// # Example
    ///
    /// ```no_run
    /// use indieweb::standards::vouch::{DomainApprover, AlwaysAllow};
    /// use indieweb::http::reqwest::Client;
    /// use indieweb::standards::vouch::DefaultVouchValidator;
    /// use std::sync::Arc;
    ///
    /// let approver = Arc::new(AlwaysAllow);
    /// let http_client = Arc::new(Client::default());
    /// let validator = DefaultVouchValidator::with_config(
    ///     approver,
    ///     http_client,
    ///     10,  // max_chain_depth
    ///     5,   // max_redirect_hops
    /// );
    /// ```
    pub fn with_config(
        approver: Arc<dyn DomainApprover>,
        http_client: Arc<dyn Client>,
        max_chain_depth: usize,
        max_redirect_hops: usize,
    ) -> Self {
        Self {
            approver,
            http_client,
            max_chain_depth,
            max_redirect_hops,
        }
    }

    /// Validates a vouch URL with the given context for chain and cycle detection.
    ///
    /// This is the core validation logic that:
    /// 1. Checks if the vouch URL is from an approved authority
    /// 2. Fetches the vouch page (following redirects)
    /// 3. Parses the page for links to the sender authority
    /// 4. Recursively validates nested vouches (if any)
    /// 5. Detects cycles to prevent infinite loops
    ///
    /// # Arguments
    ///
    /// * `vouch_url` - The URL of the vouch page to validate
    /// * `sender_authority` - The authority (scheme://host:port) of the sender
    /// * `current_depth` - Current depth in the vouch chain (for recursion)
    /// * `visited_urls` - Set of URLs visited in the current chain (for cycle detection)
    /// * `redirect_count` - Current number of redirect hops followed
    async fn validate_vouch_with_context(
        &self,
        vouch_url: &Url,
        sender_authority: &str,
        current_depth: usize,
        visited_urls: &mut HashSet<String>,
        redirect_count: usize,
    ) -> VouchValidationResult {
        // Cycle detection: check if we've already visited this URL
        let vouch_url_str = vouch_url.to_string();
        if visited_urls.contains(&vouch_url_str) {
            return VouchValidationResult::Invalid(
                "Cycle detected in vouch chain".to_string(),
            );
        }

        // Check chain depth limit
        if current_depth > self.max_chain_depth {
            return VouchValidationResult::Invalid(format!(
                "Vouch chain exceeds maximum depth of {}",
                self.max_chain_depth
            ));
        }

        // Extract authority from vouch URL
        let vouch_authority = format!(
            "{}://{}{}",
            vouch_url.scheme(),
            vouch_url.host_str().unwrap_or(""),
            vouch_url
                .port()
                .map(|p| format!(":{p}"))
                .unwrap_or_default()
        );

        // Check if vouch URL is from an approved authority
        match self
            .approver
            .is_authority_approved(&vouch_authority)
            .await
        {
            Ok(true) => {}
            Ok(false) => {
                return VouchValidationResult::Invalid(format!(
                    "Vouch URL '{}' is not from an approved authority",
                    vouch_authority
                ));
            }
            Err(e) => {
                return VouchValidationResult::Error(e);
            }
        }

        // Fetch the vouch page, following redirects
        let (final_url, body) = match self
            .fetch_page_following_redirects(vouch_url, redirect_count, visited_urls)
            .await
        {
            Ok(result) => result,
            Err(e) => return VouchValidationResult::Error(e),
        };

        // Parse the page for microformats
        let document = match from_html(&body, &final_url) {
            Ok(doc) => doc,
            Err(_) => {
                return VouchValidationResult::Invalid(
                    "Failed to parse vouch page as microformats".to_string(),
                );
            }
        };

        // Check if the page links to the sender authority
        let mut links_to_sender = false;
        let mut nested_vouch_urls: Vec<Url> = Vec::new();

        for item in &document.items {
            // Check for 'url' or 'in-reply-to' properties that might link to sender
            if let Some(urls) = item.properties.get("url") {
                if Self::property_contains_sender(urls, sender_authority) {
                    links_to_sender = true;
                }
                // Also collect these as potential nested vouch URLs
                Self::extract_urls_from_property(urls, &mut nested_vouch_urls);
            }

            if let Some(replies) = item.properties.get("in-reply-to") {
                if Self::property_contains_sender(replies, sender_authority) {
                    links_to_sender = true;
                }
                // Also collect these as potential nested vouch URLs
                Self::extract_urls_from_property(replies, &mut nested_vouch_urls);
            }
        }

        if !links_to_sender {
            return VouchValidationResult::Invalid(format!(
                "Vouch page does not link to sender authority '{}'",
                sender_authority
            ));
        }

        // Validate nested vouch URLs (chain validation)
        for nested_vouch_url in nested_vouch_urls {
            // Skip if this is the same as the sender authority
            let nested_authority = format!(
                "{}://{}{}",
                nested_vouch_url.scheme(),
                nested_vouch_url.host_str().unwrap_or(""),
                nested_vouch_url
                    .port()
                    .map(|p| format!(":{p}"))
                    .unwrap_or_default()
            );

            if nested_authority == sender_authority {
                continue;
            }

            // Recursively validate nested vouch
            let result = Box::pin(self.validate_vouch_with_context(
                &nested_vouch_url,
                sender_authority,
                current_depth + 1,
                visited_urls,
                0, // Reset redirect count for nested vouch
            ))
            .await;

            match result {
                VouchValidationResult::Valid => continue,
                VouchValidationResult::Invalid(reason) => {
                    return VouchValidationResult::Invalid(format!(
                        "Nested vouch validation failed: {}",
                        reason
                    ));
                }
                VouchValidationResult::Error(e) => {
                    return VouchValidationResult::Error(e);
                }
            }
        }

        // Mark this URL as visited after successful completion to prevent cycles in nested vouches
        visited_urls.insert(vouch_url_str);

        VouchValidationResult::Valid
    }

    /// Fetches a page, following HTTP redirects up to the maximum hop limit.
    ///
    /// # Arguments
    ///
    /// * `url` - The URL to fetch
    /// * `hop_count` - Current number of redirect hops taken
    /// * `visited_urls` - Set of URLs visited (for redirect cycle detection)
    ///
    /// # Returns
    ///
    /// A tuple of (final_url, body) if successful, or a `VouchError` if failed.
    async fn fetch_page_following_redirects(
        &self,
        url: &Url,
        hop_count: usize,
        visited_urls: &HashSet<String>,
    ) -> Result<(Url, String), VouchError> {
        // Check redirect hop limit
        if hop_count > self.max_redirect_hops {
            return Err(VouchError::Http(format!(
                "Exceeded maximum redirect hops of {}",
                self.max_redirect_hops
            )));
        }

        // Check for redirect cycle
        let url_str = url.to_string();
        if visited_urls.contains(&url_str) {
            return Err(VouchError::Http("Redirect cycle detected".to_string()));
        }

        // Create HTTP request
        let request = Request::get(url.as_str())
            .body(Body::Empty)
            .map_err(|e| VouchError::Http(format!("Failed to create request: {}", e)))?;

        // Send request
        let response = match self.http_client.send_request(request).await {
            Ok(resp) => resp,
            Err(e) => {
                return Err(VouchError::Network(format!("Failed to fetch page: {}", e)));
            }
        };

        let status = response.status();

        // Check for redirect status codes
        match status.as_u16() {
            301 | 302 | 303 | 307 | 308 => {
                // Follow redirect
                let location_header = match response.headers().get("location") {
                    Some(loc) => loc,
                    None => {
                        return Err(VouchError::Http(
                            "Redirect response missing Location header".to_string(),
                        ))
                    }
                };

                let location_str = match location_header.to_str() {
                    Ok(s) => s,
                    Err(_) => {
                        return Err(VouchError::Http(
                            "Invalid Location header in redirect".to_string(),
                        ))
                    }
                };

                let next_url = match url.join(location_str) {
                    Ok(u) => u,
                    Err(_) => {
                        return Err(VouchError::InvalidUrl(format!(
                            "Invalid redirect URL: {}",
                            location_str
                        )))
                    }
                };

                // Recursively follow redirect
                let mut new_visited = visited_urls.clone();
                new_visited.insert(url_str);
                Box::pin(self.fetch_page_following_redirects(&next_url, hop_count + 1, &new_visited))
                    .await
            }
            _ => {
                // Check for successful status
                if !status.is_success() {
                    return Err(VouchError::Http(format!(
                        "HTTP error: {}",
                        status.as_u16()
                    )));
                }

                // Extract body
                let body = match response.into_body().text() {
                    Ok(text) => text,
                    Err(e) => {
                        return Err(VouchError::Http(format!(
                            "Failed to read response body: {}",
                            e
                        )))
                    }
                };

                Ok((url.clone(), body))
            }
        }
    }

    /// Checks if a microformats property contains a URL that references the sender authority.
    ///
    /// # Arguments
    ///
    /// * `property_values` - The property values to check
    /// * `sender_authority` - The sender authority to look for
    ///
    /// # Returns
    ///
    /// `true` if any property value contains the sender authority, `false` otherwise.
    fn property_contains_sender(property_values: &[PropertyValue], sender_authority: &str) -> bool {
        for value in property_values {
            if let PropertyValue::Url(url) = value {
                if let Ok(parsed_url) = Url::parse(&url.to_string()) {
                    if Self::url_contains_sender(&parsed_url, sender_authority) {
                        return true;
                    }
                }
            }
        }
        false
    }

    /// Checks if a URL contains the sender authority.
    ///
    /// # Arguments
    ///
    /// * `url` - The URL to check
    /// * `sender_authority` - The sender authority to look for
    ///
    /// # Returns
    ///
    /// `true` if the URL contains the sender authority, `false` otherwise.
    fn url_contains_sender(url: &Url, sender_authority: &str) -> bool {
        let url_authority = format!(
            "{}://{}{}",
            url.scheme(),
            url.host_str().unwrap_or(""),
            url.port()
                .map(|p| format!(":{p}"))
                .unwrap_or_default()
        );
        url_authority == sender_authority
    }

    /// Extracts URLs from a microformats property value.
    ///
    /// # Arguments
    ///
    /// * `property_values` - The property values to extract URLs from
    /// * `urls` - Output vector to append extracted URLs to
    fn extract_urls_from_property(property_values: &[PropertyValue], urls: &mut Vec<Url>) {
        for value in property_values {
            if let PropertyValue::Url(url) = value {
                if let Ok(parsed_url) = Url::parse(&url.to_string()) {
                    urls.push(parsed_url);
                }
            }
        }
    }
}

#[async_trait]
impl VouchValidator for DefaultVouchValidator {
    /// Validates that a vouch URL properly vouches for a sender authority.
    ///
    /// This implementation performs comprehensive validation including:
    /// - Checking the vouch URL is from an approved authority
    /// - Fetching and parsing the vouch page (following redirects)
    /// - Verifying the page links to the sender authority
    /// - Recursively validating nested vouches (chain validation)
    /// - Detecting cycles in the vouch chain
    ///
    /// # Arguments
    ///
    /// * `vouch_url` - The URL of the vouch page to validate
    /// * `sender_authority` - The authority (scheme://host:port) of the sender
    ///
    /// # Returns
    ///
    /// * `Ok(VouchValidationResult::Valid)` - The vouch is valid
    /// * `Ok(VouchValidationResult::Invalid(reason))` - The vouch is invalid with a reason
    /// * `Err(VouchError)` - An error occurred during validation
    ///
    /// # Example
    ///
    /// ```no_run
    /// use indieweb::standards::vouch::{DomainApprover, AlwaysAllow, DefaultVouchValidator};
    /// use indieweb::http::reqwest::Client;
    /// use std::sync::Arc;
    /// use url::Url;
    ///
    /// #[tokio::main]
    /// async fn main() {
    ///     let approver = Arc::new(AlwaysAllow);
    ///     let http_client = Arc::new(Client::default());
    ///     let validator = DefaultVouchValidator::new(approver, http_client);
    ///
    ///     let vouch_url = Url::parse("https://example.com/vouch").unwrap();
    ///     let sender_authority = "https://sender.com";
    ///
    ///     let result = validator.validate_vouch(&vouch_url, sender_authority).await;
    ///     assert!(matches!(result.unwrap(), indieweb::standards::vouch::VouchValidationResult::Valid));
    /// }
    /// ```
    async fn validate_vouch(
        &self,
        vouch_url: &Url,
        sender_authority: &str,
    ) -> Result<VouchValidationResult, VouchError> {
        let mut visited_urls = HashSet::new();
        Ok(self
            .validate_vouch_with_context(vouch_url, sender_authority, 0, &mut visited_urls, 0)
            .await)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use http::Response;
    use indieweb::Error;

    /// Simple domain approver that approves specific domains
    struct TestDomainApprover {
        approved_domains: Vec<String>,
    }

    #[async_trait]
    impl DomainApprover for TestDomainApprover {
        async fn is_authority_approved(
            &self,
            authority: &str,
        ) -> Result<bool, VouchError> {
            Ok(self.approved_domains.iter().any(|d| authority == *d))
        }
    }

    /// Mock HTTP client for testing
    struct MockHttpClient {
        responses: std::sync::Mutex<Vec<(String, Response<Body>)>>,
    }

    #[async_trait]
    impl Client for MockHttpClient {
        async fn send_request(
            &self,
            request: Request<Body>,
        ) -> Result<Response<Body>, Error> {
            let url = request.uri().to_string();
            let mut responses = self.responses.lock().unwrap();

            for (pattern, response) in responses.iter() {
                if url.contains(pattern) {
                    return Ok(response.clone());
                }
            }

            // Default 404 response
            Ok(Response::builder()
                .status(404)
                .body(Body::Empty)
                .unwrap())
        }
    }

    /// Helper to create a mock HTTP response
    fn create_response(body: &str, status: u16) -> Response<Body> {
        Response::builder()
            .status(status)
            .body(Body::Bytes(body.as_bytes().to_vec()))
            .unwrap()
    }

    /// Helper to create a redirect response
    fn create_redirect_response(location: &str, status: u16) -> Response<Body> {
        Response::builder()
            .status(status)
            .header("location", location)
            .body(Body::Empty)
            .unwrap()
    }

    /// Helper to create a mock microformats page
    fn create_vouch_page(with_link: bool) -> String {
        if with_link {
            r#"
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="utf-8">
                <title>Vouch Page</title>
            </head>
            <body>
                <div class="h-entry">
                    <a class="u-url" href="https://sender.com/post">Link to sender</a>
                    <span class="p-name">I vouch for this person</span>
                </div>
            </body>
            </html>
            "#
            .to_string()
        } else {
            r#"
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="utf-8">
                <title>Vouch Page</title>
            </head>
            <body>
                <div class="h-entry">
                    <span class="p-name">I vouch for this person</span>
                </div>
            </body>
            </html>
            "#
            .to_string()
        }
    }

    /// Helper to create a vouch page with a nested vouch link
    fn create_nested_vouch_page(nested_url: &str) -> String {
        format!(
            r#"
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="utf-8">
                <title>Vouch Page</title>
            </head>
            <body>
                <div class="h-entry">
                    <a class="u-url" href="https://sender.com/post">Link to sender</a>
                    <span class="p-name">I vouch for this person</span>
                    <a class="u-url" href="{}">Nested vouch</a>
                </div>
            </body>
            </html>
            "#,
            nested_url
        )
    }

    #[tokio::test]
    async fn test_valid_vouch_approved_and_links() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec!["vouch.example.com".to_string()],
        });

        let mut responses = Vec::new();
        responses.push((
            "vouch.example.com".to_string(),
            create_response(&create_vouch_page(true), 200),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch.example.com/vouch").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Valid
        ));
    }

    #[tokio::test]
    async fn test_invalid_vouch_not_approved() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec!["trusted.example.com".to_string()],
        });

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(Vec::new()),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://untrusted.example.com/vouch").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Invalid(_)
        ));
    }

    #[tokio::test]
    async fn test_invalid_vouch_no_link() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec!["vouch.example.com".to_string()],
        });

        let mut responses = Vec::new();
        responses.push((
            "vouch.example.com".to_string(),
            create_response(&create_vouch_page(false), 200),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch.example.com/vouch").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Invalid(_)
        ));
    }

    #[tokio::test]
    async fn test_chain_depth_1() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec![
                "vouch1.example.com".to_string(),
                "vouch2.example.com".to_string(),
            ],
        });

        let mut responses = Vec::new();
        responses.push((
            "vouch1.example.com".to_string(),
            create_response(&create_nested_vouch_page("https://vouch2.example.com/vouch"), 200),
        ));
        responses.push((
            "vouch2.example.com".to_string(),
            create_response(&create_vouch_page(true), 200),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch1.example.com/vouch").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Valid
        ));
    }

    #[tokio::test]
    async fn test_chain_depth_5() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec![
                "vouch1.example.com".to_string(),
                "vouch2.example.com".to_string(),
                "vouch3.example.com".to_string(),
                "vouch4.example.com".to_string(),
                "vouch5.example.com".to_string(),
            ],
        });

        let mut responses = Vec::new();
        // Build chain: vouch1 -> vouch2 -> vouch3 -> vouch4 -> vouch5 -> (links to sender)
        responses.push((
            "vouch1.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch2.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch2.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch3.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch3.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch4.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch4.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch5.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch5.example.com".to_string(),
            create_response(&create_vouch_page(true), 200),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch1.example.com/vouch").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Valid
        ));
    }

    #[tokio::test]
    async fn test_chain_exceeds_max_depth() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec![
                "vouch1.example.com".to_string(),
                "vouch2.example.com".to_string(),
                "vouch3.example.com".to_string(),
                "vouch4.example.com".to_string(),
                "vouch5.example.com".to_string(),
                "vouch6.example.com".to_string(),
            ],
        });

        let mut responses = Vec::new();
        // Build chain that exceeds max depth (5): vouch1 -> vouch2 -> vouch3 -> vouch4 -> vouch5 -> vouch6
        responses.push((
            "vouch1.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch2.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch2.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch3.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch3.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch4.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch4.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch5.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch5.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch6.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch6.example.com".to_string(),
            create_response(&create_vouch_page(true), 200),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch1.example.com/vouch").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Invalid(_)
        ));
    }

    #[tokio::test]
    async fn test_cycle_detection_a_b_c_a() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec![
                "vouch1.example.com".to_string(),
                "vouch2.example.com".to_string(),
                "vouch3.example.com".to_string(),
            ],
        });

        let mut responses = Vec::new();
        // Build cycle: vouch1 -> vouch2 -> vouch3 -> vouch1
        responses.push((
            "vouch1.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch2.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch2.example.com".to_string(),
            create_response(
                &create_nested_vouch_page("https://vouch3.example.com/vouch"),
                200,
            ),
        ));
        responses.push((
            "vouch3.example.com".to_string(),
            create_response(&create_nested_vouch_page("https://vouch1.example.com/vouch"), 200),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch1.example.com/vouch").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Invalid(_)
        ));
    }

    #[tokio::test]
    async fn test_redirect_1_hop() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec!["vouch.example.com".to_string()],
        });

        let mut responses = Vec::new();
        responses.push((
            "vouch.example.com/redirect".to_string(),
            create_redirect_response("https://vouch.example.com/vouch", 302),
        ));
        responses.push((
            "vouch.example.com/vouch".to_string(),
            create_response(&create_vouch_page(true), 200),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch.example.com/redirect").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Valid
        ));
    }

    #[tokio::test]
    async fn test_redirect_3_hops() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec!["vouch.example.com".to_string()],
        });

        let mut responses = Vec::new();
        // Build redirect chain: /r1 -> /r2 -> /r3 -> /vouch
        responses.push((
            "vouch.example.com/r1".to_string(),
            create_redirect_response("/r2", 301),
        ));
        responses.push((
            "vouch.example.com/r2".to_string(),
            create_redirect_response("/r3", 307),
        ));
        responses.push((
            "vouch.example.com/r3".to_string(),
            create_redirect_response("/vouch", 308),
        ));
        responses.push((
            "vouch.example.com/vouch".to_string(),
            create_response(&create_vouch_page(true), 200),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch.example.com/r1").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Valid
        ));
    }

    #[tokio::test]
    async fn test_redirect_too_many_hops() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec!["vouch.example.com".to_string()],
        });

        let mut responses = Vec::new();
        // Build redirect chain that exceeds max hops (3): /r1 -> /r2 -> /r3 -> /r4 -> /vouch
        responses.push((
            "vouch.example.com/r1".to_string(),
            create_redirect_response("/r2", 302),
        ));
        responses.push((
            "vouch.example.com/r2".to_string(),
            create_redirect_response("/r3", 301),
        ));
        responses.push((
            "vouch.example.com/r3".to_string(),
            create_redirect_response("/r4", 307),
        ));
        responses.push((
            "vouch.example.com/r4".to_string(),
            create_redirect_response("/vouch", 308),
        ));
        responses.push((
            "vouch.example.com/vouch".to_string(),
            create_response(&create_vouch_page(true), 200),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch.example.com/r1").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Invalid(_)
        ));
    }

    #[tokio::test]
    async fn test_redirect_cycle_a_b_a() {
        let approver = Arc::new(TestDomainApprover {
            approved_domains: vec!["vouch.example.com".to_string()],
        });

        let mut responses = Vec::new();
        // Build redirect cycle: /a -> /b -> /a
        responses.push((
            "vouch.example.com/a".to_string(),
            create_redirect_response("/b", 302),
        ));
        responses.push((
            "vouch.example.com/b".to_string(),
            create_redirect_response("/a", 301),
        ));

        let http_client = Arc::new(MockHttpClient {
            responses: std::sync::Mutex::new(responses),
        });

        let validator = DefaultVouchValidator::new(approver, http_client);

        let vouch_url = Url::parse("https://vouch.example.com/a").unwrap();
        let sender_authority = "https://sender.com";

        let result = validator.validate_vouch(&vouch_url, sender_authority).await;

        assert!(matches!(
            result.unwrap(),
            VouchValidationResult::Invalid(_)
        ));
    }
}
