use eyre::{Result, eyre};
use std::{collections::HashSet, sync::Arc};

use crate::context::AppContext;

pub struct Crawler {
    pub ctx: Arc<AppContext>,
    pub visited: HashSet<String>,
}

impl Crawler {
    pub fn new(ctx: Arc<AppContext>) -> Self {
        Crawler {
            ctx,
            visited: HashSet::new(),
        }
    }

    pub async fn crawl(&mut self, url: &str) -> Result<Vec<String>> {
        if !self.visited.insert(url.to_string()) {
            return Ok(Vec::new()); // already crawled this page
        }

        let html = self
            .ctx
            .http_client
            .fetch(url)
            .await
            .map_err(|e| eyre!("failed to fetch URL {}: {}", url, e))?;

        let links = self
            .ctx
            .parser
            .parse_links(&html)
            .map_err(|e| eyre!("failed to parse links: {}", e))?;

        Ok(links)
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use crate::context::AppContext;
    use crate::settings::Settings;

    use super::*;

    #[tokio::test]
    async fn test_crawl_already_visited_returns_empty() {
        let settings = Settings::default();
        let ctx = Arc::new(AppContext::new(settings));
        let mut crawler = Crawler::new(ctx);
        let url = "https://example.com";
        crawler.visited.insert(url.to_string());
        let result = crawler.crawl(url).await;
        assert!(result.is_ok());
        assert!(result.unwrap().is_empty());
    }

    #[tokio::test]
    async fn test_crawl_records_visited() {
        let mock_server = wiremock::MockServer::start().await;
        let body = r#"<html><body><a href="/a">A</a></body></html>"#;
        wiremock::Mock::given(wiremock::matchers::method("GET"))
            .and(wiremock::matchers::path("/"))
            .respond_with(wiremock::ResponseTemplate::new(200).set_body_string(body))
            .mount(&mock_server)
            .await;

        let mut settings = Settings::default();
        settings.http.timeout_seconds = 30;
        let ctx = Arc::new(AppContext::new(settings));
        let mut crawler = Crawler::new(ctx);
        let url = format!("{}/", mock_server.uri());

        let links = crawler.crawl(&url).await.unwrap();
        assert_eq!(crawler.visited.len(), 1);
        assert_eq!(links.len(), 1);
        assert_eq!(links[0], "/a");
    }
}
