use super::{checkpoint::Checkpoint, utils};
use crate::{Info, extractor::ExtractorError};
use scraper::Html;
use serde::{Deserialize, Serialize};
use std::collections::{HashSet, VecDeque};
use url::Url;
use validator::Validate;

#[derive(Clone, Debug, Default, Serialize, Deserialize, Validate, schemars::JsonSchema)]
struct TestInfo {
    value: String,
}

impl Info for TestInfo {
    fn json_schema() -> serde_json::Value {
        serde_json::json!({"type": "object"})
    }

    fn post_process(&mut self, _original_text: &str) -> Result<(), ExtractorError> {
        Ok(())
    }
}

#[test]
fn page_entry_includes_contact_links_and_visible_email_once() {
    let url = Url::parse("https://example.com").unwrap();
    let html = r#"
        <html><body>
            <a href="mailto:info@example.com?subject=Hi">Email</a>
            <a href="tel:+441234567890">Call</a>
            <p>Contact info@example.com for details.</p>
        </body></html>
    "#;

    let entry = utils::page_entry(&url, html);

    assert!(entry.contains("URL: https://example.com/"));
    assert!(entry.contains("Emails: info@example.com"));
    assert!(entry.contains("Phones: +441234567890"));
    assert_eq!(entry.matches("info@example.com").count(), 2);
}

#[test]
fn page_links_resolves_relative_urls() {
    let url = Url::parse("https://example.com/root/page").unwrap();
    let links = utils::page_links(&url, r#"<a href="../contact">Contact</a>"#);

    assert_eq!(links, [Url::parse("https://example.com/contact").unwrap()]);
}

#[test]
fn duplicate_path_segments_are_deduplicated() {
    let url = Url::parse("https://example.com/a/a/b/b/c").unwrap();
    let deduped = utils::deduplicate_url_segments(&url).unwrap();

    assert!(utils::has_duplicate_segments(&url));
    assert_eq!(deduped.as_str(), "https://example.com/a/b/c");
}

#[test]
fn checkpoint_appends_source_text_and_replaces_results() {
    let mut checkpoint = Checkpoint::<TestInfo>::new(Url::parse("https://example.com").unwrap());
    let mut targets = HashSet::new();
    targets.insert(Url::parse("https://target.example").unwrap());

    checkpoint.add_target_urls(&targets);
    checkpoint.add_source_text("target.example".to_string(), "first".to_string());
    checkpoint.add_source_text("target.example".to_string(), "second".to_string());
    checkpoint.add_result(
        "target.example".to_string(),
        TestInfo {
            value: "old".into(),
        },
    );
    checkpoint.add_result(
        "target.example".to_string(),
        TestInfo {
            value: "new".into(),
        },
    );

    assert_eq!(checkpoint.target_urls, targets);
    assert_eq!(
        checkpoint.get_source_text("target.example").unwrap(),
        "first\nsecond"
    );
    assert_eq!(checkpoint.results.len(), 1);
    assert_eq!(checkpoint.results[0].1.value, "new");
}

#[test]
fn extract_contact_links_deduplicates_scheme_values() {
    let html = Html::parse_document(
        r#"
        <a href="mailto:info@example.com">One</a>
        <a href="mailto:info@example.com?subject=Hello">Two</a>
        <a href="tel:+441234567890">Call</a>
        "#,
    );

    let (emails, phones) = utils::extract_contact_links(&html);

    assert_eq!(emails, ["info@example.com"]);
    assert_eq!(phones, ["+441234567890"]);
}

#[test]
fn scope_matching_uses_start_url_root_not_current_page_path() {
    assert!(super::is_within_scope("/exhibitors/page/2", "/exhibitors"));
    assert!(super::is_within_scope(
        "/exhibitors/company/acme",
        "/exhibitors"
    ));
    assert!(!super::is_within_scope("/exhibitor/acme", "/exhibitors"));
}

#[test]
fn scope_normalization_treats_root_and_trailing_slashes_consistently() {
    assert_eq!(super::normalize_scope_path("/"), "/");
    assert_eq!(super::normalize_scope_path("/exhibitors/"), "/exhibitors");
    assert_eq!(super::normalize_scope_path("/exhibitors"), "/exhibitors");
}

#[test]
fn same_path_query_variants_remain_in_scope() {
    assert!(super::is_within_scope("/exhibitor-list", "/exhibitor-list"));
}

#[test]
fn crawler_config_defaults_to_sequential_fetching() {
    let config = super::CrawlerConfig::default();

    assert_eq!(config.max_concurrency, 1);
}

#[cfg(feature = "query-helpers")]
#[test]
fn tracking_query_keys_are_ignored_by_meaningful_query_pairs() {
    let url = Url::parse("https://example.com/exhibitors?page=2&utm_source=newsletter&fbclid=123")
        .unwrap();
    let pairs = utils::meaningful_query_pairs(&url).collect::<Vec<_>>();

    assert_eq!(pairs.len(), 1);
    assert_eq!(pairs[0].0, "page");
    assert_eq!(pairs[0].1, "2");
}

#[cfg(feature = "query-helpers")]
#[test]
fn query_keys_allowed_rejects_non_allowlisted_keys() {
    let paged = Url::parse("https://example.com/exhibitors?page=2&offset=20").unwrap();
    let sorted = Url::parse("https://example.com/exhibitors?page=2&sort=title").unwrap();

    assert!(utils::query_keys_allowed(&paged, &["page", "offset"]));
    assert!(!utils::query_keys_allowed(&sorted, &["page", "offset"]));
}

#[cfg(feature = "query-helpers")]
#[test]
fn keyword_score_sums_matching_keyword_weights() {
    let score = utils::keyword_score(
        "/events/exhibitor-directory?page=2",
        &[("exhibitor", 10), ("directory", 5), ("speaker", -10)],
    );

    assert_eq!(score, 15);
}

#[cfg(feature = "query-helpers")]
#[test]
fn url_pattern_memory_tracks_successful_url_shapes() {
    let mut memory = utils::UrlPatternMemory::default();
    let successful = Url::parse("https://example.com/exhibitors/acme?page=2").unwrap();
    let similar = Url::parse("https://example.com/exhibitors/beta?page=3").unwrap();
    let different = Url::parse("https://example.com/news/launch").unwrap();

    memory.record_success(&successful);

    assert!(memory.has_signals());
    assert!(memory.best_similarity_score(&similar) > memory.best_similarity_score(&different));
}

#[cfg(feature = "url-priority")]
#[test]
fn url_priority_inserts_higher_scores_earlier() {
    let config = super::CrawlerConfig {
        url_priority: Some(Box::new(|url| {
            if url.path().contains("priority") {
                10
            } else {
                0
            }
        })),
        ..Default::default()
    };
    let crawler = super::Crawler::new(config).unwrap();
    let mut visited = HashSet::new();
    let mut queue = VecDeque::new();

    crawler.enqueue_if_new(
        Url::parse("https://example.com/normal").unwrap(),
        &mut visited,
        &mut queue,
    );
    crawler.enqueue_if_new(
        Url::parse("https://example.com/priority").unwrap(),
        &mut visited,
        &mut queue,
    );

    assert_eq!(queue.pop_front().unwrap().path(), "/priority");
    assert_eq!(queue.pop_front().unwrap().path(), "/normal");
}
