use super::{
    App,
    runtime::Runtime,
    utils::{CONTENT_PLATFORM_DOMAINS, SOCIAL_MEDIA_DOMAINS, dedup_results, is_external_url},
};
use crate::{Info, crawler::checkpoint::Checkpoint, extractor::ExtractorError, gui::GuiError};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
    path::PathBuf,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
};
use tokio::runtime::Runtime as TokioRuntime;
use tokio_util::sync::CancellationToken;
use url::Url;
use validator::Validate;

#[derive(Clone)]
struct TestRuntime;

#[derive(Clone)]
struct FallbackRuntime;

#[derive(Clone, Default, Serialize, Deserialize, Validate, 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(())
    }
}

impl Runtime<TestInfo> for TestRuntime {
    async fn run(
        &self,
        _config: super::runtime::RuntimeConfig,
        _cancel: CancellationToken,
    ) -> Result<(), GuiError> {
        Ok(())
    }
}

impl Runtime<TestInfo> for FallbackRuntime {
    async fn run(
        &self,
        _config: super::runtime::RuntimeConfig,
        _cancel: CancellationToken,
    ) -> Result<(), GuiError> {
        Ok(())
    }

    fn show_url_input(&self) -> bool {
        false
    }

    fn fallback_start_url(&self) -> Option<Url> {
        Url::parse("https://import.invalid/").ok()
    }
}

#[test]
fn dedup_results_normalizes_www_prefix_and_case() {
    let results = vec![
        ("www.Example.com".to_string(), 1),
        ("example.com".to_string(), 2),
        ("other.com".to_string(), 3),
    ];

    let deduped = dedup_results(results);

    assert_eq!(
        deduped,
        [
            ("www.Example.com".to_string(), 1),
            ("other.com".to_string(), 3)
        ]
    );
}

#[test]
fn external_url_rejects_blocked_and_asset_hosts() {
    let blocked = Url::parse("https://linkedin.com/company/example").unwrap();
    let cdn = Url::parse("https://cdn.example.com/app.js").unwrap();
    let company = Url::parse("https://example-company.com").unwrap();

    assert!(!is_external_url(&blocked, &[SOCIAL_MEDIA_DOMAINS]));
    assert!(!is_external_url(&cdn, &[SOCIAL_MEDIA_DOMAINS]));
    assert!(is_external_url(&company, &[SOCIAL_MEDIA_DOMAINS]));
}

#[test]
fn external_url_allows_hosted_business_platform_domains() {
    let wordpress = Url::parse("https://acme.wordpress.com/about").unwrap();
    let shopify = Url::parse("https://acme.myshopify.com/pages/about").unwrap();

    assert!(is_external_url(
        &wordpress,
        &[SOCIAL_MEDIA_DOMAINS, CONTENT_PLATFORM_DOMAINS]
    ));
    assert!(is_external_url(
        &shopify,
        &[SOCIAL_MEDIA_DOMAINS, CONTENT_PLATFORM_DOMAINS]
    ));
}

#[test]
fn refresh_checkpoints_if_needed_updates_available_checkpoints() {
    let temp_dir = std::env::temp_dir().join(format!("ras_crawl_gui_test_{}", std::process::id()));
    std::fs::create_dir_all(&temp_dir).unwrap();
    let checkpoint_path = temp_dir.join("checkpoint_example");
    std::fs::write(&checkpoint_path, b"checkpoint").unwrap();

    let runtime = TokioRuntime::new().unwrap();
    let mut app = App::new(runtime.handle().clone(), &["test_crawler"], TestRuntime).unwrap();
    app.working_directory = temp_dir.display().to_string();
    app.available_checkpoints = Vec::<PathBuf>::new();
    app.checkpoints_need_refresh = Arc::new(AtomicBool::new(true));

    app.refresh_checkpoints_if_needed();

    assert_eq!(app.available_checkpoints, vec![checkpoint_path.clone()]);
    assert!(!app.checkpoints_need_refresh.load(Ordering::Relaxed));

    std::fs::remove_file(checkpoint_path).unwrap();
    std::fs::remove_dir(temp_dir).unwrap();
}

#[test]
fn scan_checkpoints_accepts_dotful_checkpoint_names() {
    let temp_dir = std::env::temp_dir().join(format!(
        "ras_crawl_checkpoint_scan_test_{}",
        std::process::id()
    ));
    std::fs::create_dir_all(&temp_dir).unwrap();
    let checkpoint = temp_dir.join("checkpoint_www.installershow.com_2026_04_21_14_09_24");
    let log_file = temp_dir.join("crawler_www.installershow.com_2026_04_21_14_09_24.log");
    std::fs::write(&checkpoint, b"checkpoint").unwrap();
    std::fs::write(&log_file, b"log").unwrap();

    let scanned = super::utils::scan_checkpoints(temp_dir.to_str().unwrap());

    assert_eq!(scanned, vec![checkpoint.clone()]);

    std::fs::remove_file(checkpoint).unwrap();
    std::fs::remove_file(log_file).unwrap();
    std::fs::remove_dir(temp_dir).unwrap();
}

#[test]
fn checkpoint_display_name_formats_host_and_timestamp() {
    let path = PathBuf::from("checkpoint_www.installershow.com_2026_04_21_14_09_24");

    let display = super::utils::checkpoint_display_name(&path);

    assert_eq!(display, "www.installershow.com 2026-04-21 14:09:24");
}

#[test]
fn resolve_start_url_uses_checkpoint_when_url_is_empty() {
    let temp_dir = std::env::temp_dir().join(format!(
        "ras_crawl_resolve_start_url_test_{}",
        std::process::id()
    ));
    std::fs::create_dir_all(&temp_dir).unwrap();
    let checkpoint_path = temp_dir.join("checkpoint_example");
    let checkpoint =
        Checkpoint::<TestInfo>::new(Url::parse("https://show.example/exhibitors").unwrap());
    checkpoint.save(&checkpoint_path).unwrap();

    let runtime = TokioRuntime::new().unwrap();
    let mut app = App::new(runtime.handle().clone(), &["test_crawler"], TestRuntime).unwrap();
    app.url.clear();
    app.checkpoint_path = Some(checkpoint_path.display().to_string());

    let resolved = app.resolve_start_url().unwrap();

    assert_eq!(resolved.as_str(), "https://show.example/exhibitors");

    std::fs::remove_file(checkpoint_path).unwrap();
    std::fs::remove_dir(temp_dir).unwrap();
}

#[test]
fn resolve_start_url_uses_runtime_fallback_when_url_is_empty() {
    let runtime = TokioRuntime::new().unwrap();
    let app = App::new(runtime.handle().clone(), &["test_crawler"], FallbackRuntime).unwrap();

    let resolved = app.resolve_start_url().unwrap();

    assert_eq!(resolved.as_str(), "https://import.invalid/");
}
