use std::sync::Arc;

use actix_web::{App, HttpRequest, HttpResponse, HttpServer, dev::ServerHandle, web};
use browsr_client::{
    BrowsrClient, BrowsrClientConfig, ObserveOptions, TransportConfig, default_base_url,
    default_transport,
};
use browsr_types::{
    AutomateResponse, BrowserContext, CommandData, CommandResult, CommandType, Commands,
    DomSnapshot, ObserveResponse, PageMetadata, ScrapeApiRequest, ScrapeApiResponse, ScrapeData,
    SearchOptions, SearchResponse, SearchResultEntry,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::Mutex;

#[derive(Default)]
struct TestServerState {
    command_payloads: Mutex<Vec<Value>>,
    observe_payloads: Mutex<Vec<Value>>,
    scrape_payloads: Mutex<Vec<Value>>,
    search_payloads: Mutex<Vec<Value>>,
    deleted_sessions: Mutex<Vec<String>>,
    api_key_headers: Mutex<Vec<Option<String>>>,
}

struct TestServer {
    base_url: String,
    state: Arc<TestServerState>,
    handle: ServerHandle,
}

impl Drop for TestServer {
    fn drop(&mut self) {
        let handle = self.handle.clone();
        tokio::spawn(async move {
            let _ = handle.stop(true).await;
        });
    }
}

#[tokio::test]
async fn http_client_sessions_and_commands() -> anyhow::Result<()> {
    let server = spawn_test_server().await;
    let config = BrowsrClientConfig::new(&server.base_url).with_headless(false);
    let client = BrowsrClient::from_client_config(config);

    let sessions = client.list_sessions().await?;
    assert_eq!(sessions.sessions, vec!["session-1".to_string()]);

    let session_id = client.create_session().await?;
    assert_eq!(session_id.session_id, "session-1");

    client.destroy_session(&session_id.session_id).await?;

    let commands = vec![
        Commands::NavigateTo {
            url: "https://example.com".to_string(),
        },
        Commands::GetTitle,
    ];
    let response = client
        .execute_commands(commands, Some("session-1".to_string()), None, None)
        .await?;
    assert!(response.success);

    client
        .execute_command(
            Commands::Click {
                selector: "#cta".to_string(),
            },
            None,
            Some(true),
        )
        .await?;
    client.navigate("https://example.com/next", None).await?;
    client.click(".item", None).await?;
    client
        .type_text("#input", "hello", Some(true), None)
        .await?;
    client.wait_for_element("#loaded", Some(1500), None).await?;
    client.screenshot(true, None).await?;
    client.get_title(None).await?;
    client.get_text("h1", None).await?;
    client
        .get_content(Some(".content".to_string()), None)
        .await?;
    client.evaluate("1 + 1", None).await?;
    client
        .extract_structured("extract data", None, Some(5000), None)
        .await?;

    let payloads = server.state.command_payloads.lock().await.clone();
    assert_eq!(payloads.len(), 12);

    let first = parse_commands_payload(&payloads[0])?;
    assert_eq!(first.commands.len(), 2);
    assert!(matches!(first.commands[0], Commands::NavigateTo { .. }));
    assert!(matches!(first.commands[1], Commands::GetTitle));
    assert_eq!(first.session_id.as_deref(), Some("session-1"));
    assert_eq!(first.headless, Some(false));

    let expected = vec![
        ExpectedCommand::Click("#cta".to_string()),
        ExpectedCommand::Navigate("https://example.com/next".to_string()),
        ExpectedCommand::Click(".item".to_string()),
        ExpectedCommand::TypeText {
            selector: "#input".to_string(),
            text: "hello".to_string(),
            clear: Some(true),
        },
        ExpectedCommand::WaitForElement {
            selector: "#loaded".to_string(),
            timeout_ms: Some(1500),
        },
        ExpectedCommand::Screenshot {
            full_page: Some(true),
        },
        ExpectedCommand::GetTitle,
        ExpectedCommand::GetText("h1".to_string()),
        ExpectedCommand::GetContent(Some(".content".to_string())),
        ExpectedCommand::Evaluate("1 + 1".to_string()),
        ExpectedCommand::ExtractStructured {
            query: "extract data".to_string(),
            max_chars: Some(5000),
        },
    ];

    for (payload, expected) in payloads.iter().skip(1).zip(expected.iter()) {
        let parsed = parse_commands_payload(payload)?;
        assert_eq!(parsed.commands.len(), 1);
        assert_command(&parsed.commands[0], expected);
    }

    Ok(())
}

#[tokio::test]
async fn http_client_observe_scrape_search() -> anyhow::Result<()> {
    let server = spawn_test_server().await;
    let client = BrowsrClient::new(&server.base_url);

    let observe = client
        .observe(
            Some("session-1".to_string()),
            None,
            ObserveOptions::default(),
        )
        .await?;
    assert_eq!(observe.dom_snapshot.url, "https://example.com");

    let scrape = client.scrape_url("https://example.com").await?;
    assert!(scrape.success);

    let _ = client.scrape_url("https://example.com/2").await?;

    let search = client
        .search(SearchOptions {
            query: "test".to_string(),
            limit: Some(3),
        })
        .await?;
    assert!(search.success);

    let _ = client.search_query("extra").await?;

    let observe_payloads = server.state.observe_payloads.lock().await.clone();
    assert_eq!(observe_payloads.len(), 1);
    let observe_payload: ObservePayload = serde_json::from_value(observe_payloads[0].clone())?;
    assert_eq!(observe_payload.session_id.as_deref(), Some("session-1"));
    assert_eq!(observe_payload.use_image, Some(true));
    assert_eq!(observe_payload.include_content, Some(true));

    let scrape_payloads = server.state.scrape_payloads.lock().await.clone();
    assert_eq!(scrape_payloads.len(), 2);
    let first_scrape: ScrapeApiRequest = serde_json::from_value(scrape_payloads[0].clone())?;
    assert_eq!(first_scrape.url, "https://example.com");
    let second_scrape: ScrapeApiRequest = serde_json::from_value(scrape_payloads[1].clone())?;
    assert_eq!(second_scrape.url, "https://example.com/2");

    let search_payloads = server.state.search_payloads.lock().await.clone();
    assert_eq!(search_payloads.len(), 2);
    let first_search: SearchOptions = serde_json::from_value(search_payloads[0].clone())?;
    assert_eq!(first_search.query, "test");
    assert_eq!(first_search.limit, Some(3));
    let second_search: SearchOptions = serde_json::from_value(search_payloads[1].clone())?;
    assert_eq!(second_search.query, "extra");
    assert_eq!(second_search.limit, None);

    Ok(())
}

#[tokio::test]
async fn http_client_sends_api_key_header() -> anyhow::Result<()> {
    let server = spawn_test_server().await;
    let client = BrowsrClient::new(&server.base_url).with_api_key("secret");

    let _ = client.list_sessions().await?;
    let headers = server.state.api_key_headers.lock().await.clone();
    assert_eq!(headers.len(), 1);
    assert_eq!(headers[0].as_deref(), Some("secret"));

    Ok(())
}

#[tokio::test]
async fn client_env_helpers() -> anyhow::Result<()> {
    let guard_url = EnvGuard::set("BROWSR_BASE_URL", "http://localhost:9999");
    let guard_api = EnvGuard::set("BROWSR_API_KEY", "key");
    let client = BrowsrClient::from_env();
    assert_eq!(client.base_url(), "http://localhost:9999");
    assert!(client.has_auth());
    drop(guard_url);
    drop(guard_api);

    let local_client = BrowsrClient::new("http://localhost:8082");
    assert!(local_client.is_local());
    assert_eq!(local_client.config().base_url, "http://localhost:8082");

    let default_url = default_base_url().expect("default base url");
    assert!(!default_url.is_empty());

    let transport = default_transport();
    match transport {
        TransportConfig::Http { base_url } => {
            assert!(!base_url.is_empty());
        }
    }

    let configured = BrowsrClient::from_config(TransportConfig::Http {
        base_url: "http://localhost:8082".to_string(),
    });
    assert!(configured.is_local());

    Ok(())
}

async fn spawn_test_server() -> TestServer {
    let state = Arc::new(TestServerState::default());
    let state_clone = state.clone();
    let server = HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(state_clone.clone()))
            .route("/sessions", web::get().to(list_sessions))
            .route("/sessions", web::post().to(create_session))
            .route("/sessions/{id}", web::delete().to(delete_session))
            .route("/commands", web::post().to(commands_handler))
            .route("/observe", web::post().to(observe_handler))
            .route("/v1/scrape", web::post().to(scrape_v1_handler))
            .route("/search", web::post().to(search_handler))
    })
    .bind(("127.0.0.1", 0))
    .expect("failed to bind test server");

    let port = server.addrs()[0].port();
    let handle = server.run();
    let server_handle = handle.handle();

    tokio::spawn(async move {
        let _ = handle.await;
    });

    TestServer {
        base_url: format!("http://127.0.0.1:{port}"),
        state,
        handle: server_handle,
    }
}

async fn list_sessions(state: web::Data<Arc<TestServerState>>, req: HttpRequest) -> HttpResponse {
    record_api_key(&state, &req).await;
    HttpResponse::Ok().json(serde_json::json!({
        "sessions": ["session-1"]
    }))
}

async fn create_session(state: web::Data<Arc<TestServerState>>, req: HttpRequest) -> HttpResponse {
    record_api_key(&state, &req).await;
    HttpResponse::Ok().json(serde_json::json!({
        "session_id": "session-1"
    }))
}

async fn delete_session(
    state: web::Data<Arc<TestServerState>>,
    path: web::Path<String>,
) -> HttpResponse {
    state.deleted_sessions.lock().await.push(path.into_inner());
    HttpResponse::NoContent().finish()
}

async fn commands_handler(
    state: web::Data<Arc<TestServerState>>,
    body: web::Json<Value>,
) -> HttpResponse {
    state.command_payloads.lock().await.push(body.into_inner());
    HttpResponse::Ok().json(mock_automate_response())
}

async fn observe_handler(
    state: web::Data<Arc<TestServerState>>,
    body: web::Json<Value>,
) -> HttpResponse {
    state.observe_payloads.lock().await.push(body.into_inner());
    HttpResponse::Ok().json(mock_observe_envelope())
}

async fn scrape_v1_handler(
    state: web::Data<Arc<TestServerState>>,
    body: web::Json<Value>,
) -> HttpResponse {
    state.scrape_payloads.lock().await.push(body.into_inner());
    HttpResponse::Ok().json(mock_scrape_api_response())
}

async fn search_handler(
    state: web::Data<Arc<TestServerState>>,
    body: web::Json<Value>,
) -> HttpResponse {
    state.search_payloads.lock().await.push(body.into_inner());
    HttpResponse::Ok().json(mock_search_response())
}

async fn record_api_key(state: &web::Data<Arc<TestServerState>>, req: &HttpRequest) {
    let header = req
        .headers()
        .get("x-api-key")
        .and_then(|v| v.to_str().ok())
        .map(|s| s.to_string());
    state.api_key_headers.lock().await.push(header);
}

#[derive(Debug, Deserialize)]
struct CommandsPayload {
    commands: Vec<Commands>,
    session_id: Option<String>,
    headless: Option<bool>,
    context: Option<BrowserContext>,
}

#[derive(Debug, Deserialize)]
struct ObservePayload {
    session_id: Option<String>,
    headless: Option<bool>,
    use_image: Option<bool>,
    full_page: Option<bool>,
    wait_ms: Option<u64>,
    include_content: Option<bool>,
}

#[derive(Debug)]
enum ExpectedCommand {
    Navigate(String),
    Click(String),
    TypeText {
        selector: String,
        text: String,
        clear: Option<bool>,
    },
    WaitForElement {
        selector: String,
        timeout_ms: Option<u64>,
    },
    Screenshot {
        full_page: Option<bool>,
    },
    GetTitle,
    GetText(String),
    GetContent(Option<String>),
    Evaluate(String),
    ExtractStructured {
        query: String,
        max_chars: Option<usize>,
    },
}

fn assert_command(command: &Commands, expected: &ExpectedCommand) {
    match (command, expected) {
        (Commands::NavigateTo { url }, ExpectedCommand::Navigate(expected_url)) => {
            assert_eq!(url, expected_url)
        }
        (Commands::Click { selector }, ExpectedCommand::Click(expected)) => {
            assert_eq!(selector, expected)
        }
        (
            Commands::TypeText {
                selector,
                text,
                clear,
            },
            ExpectedCommand::TypeText {
                selector: expected_selector,
                text: expected_text,
                clear: expected_clear,
            },
        ) => {
            assert_eq!(selector, expected_selector);
            assert_eq!(text, expected_text);
            assert_eq!(clear, expected_clear);
        }
        (
            Commands::WaitForElement {
                selector,
                timeout_ms,
                ..
            },
            ExpectedCommand::WaitForElement {
                selector: expected_selector,
                timeout_ms: expected_timeout,
            },
        ) => {
            assert_eq!(selector, expected_selector);
            assert_eq!(timeout_ms, expected_timeout);
        }
        (
            Commands::Screenshot { full_page, .. },
            ExpectedCommand::Screenshot {
                full_page: expected,
            },
        ) => {
            assert_eq!(full_page, expected);
        }
        (Commands::GetTitle, ExpectedCommand::GetTitle) => {}
        (Commands::GetText { selector }, ExpectedCommand::GetText(expected)) => {
            assert_eq!(selector, expected)
        }
        (Commands::GetContent { selector, .. }, ExpectedCommand::GetContent(expected)) => {
            assert_eq!(selector, expected)
        }
        (Commands::Evaluate { expression }, ExpectedCommand::Evaluate(expected)) => {
            assert_eq!(expression, expected)
        }
        (
            Commands::ExtractStructuredContent {
                query, max_chars, ..
            },
            ExpectedCommand::ExtractStructured {
                query: expected_query,
                max_chars: expected_max,
            },
        ) => {
            assert_eq!(query, expected_query);
            assert_eq!(max_chars, expected_max);
        }
        _ => panic!("unexpected command payload: {:?}", command),
    }
}

fn parse_commands_payload(payload: &Value) -> anyhow::Result<CommandsPayload> {
    Ok(serde_json::from_value(payload.clone())?)
}

fn mock_automate_response() -> AutomateResponse {
    AutomateResponse {
        success: true,
        steps_completed: 1,
        total_steps: 1,
        results: vec![CommandResult {
            success: true,
            command_type: CommandType::Action,
            data: Some(CommandData::String("ok".to_string())),
            error: None,
            debug: None,
        }],
        error: None,
    }
}

#[derive(Serialize)]
struct ObserveEnvelope {
    session_id: String,
    observation: ObserveResponse,
}

fn mock_observe_envelope() -> ObserveEnvelope {
    ObserveEnvelope {
        session_id: "session-1".to_string(),
        observation: ObserveResponse {
            dom_snapshot: DomSnapshot {
                url: "https://example.com".to_string(),
                title: Some("Example".to_string()),
                viewport: (1280, 720),
                captured_at: 0,
                interactive: Vec::new(),
                interactive_text: None,
            },
            state_text: "state".to_string(),
            screenshot: None,
            click_targets: Vec::new(),
        },
    }
}

fn mock_scrape_api_response() -> ScrapeApiResponse {
    ScrapeApiResponse {
        success: true,
        data: ScrapeData {
            metadata: PageMetadata {
                title: None,
                description: None,
                source_url: "https://example.com".to_string(),
                status_code: Some(200),
            },
            ..Default::default()
        },
    }
}

fn mock_search_response() -> SearchResponse {
    SearchResponse {
        success: true,
        total: 1,
        data: vec![SearchResultEntry {
            title: "Result".to_string(),
            url: "https://example.com".to_string(),
            description: "content".to_string(),
            position: 1,
            content: None,
        }],
    }
}

struct EnvGuard {
    key: &'static str,
    previous: Option<String>,
}

impl EnvGuard {
    fn set(key: &'static str, value: &str) -> Self {
        let previous = std::env::var(key).ok();
        unsafe {
            std::env::set_var(key, value);
        }
        Self { key, previous }
    }
}

impl Drop for EnvGuard {
    fn drop(&mut self) {
        if let Some(value) = self.previous.take() {
            unsafe {
                std::env::set_var(self.key, value);
            }
        } else {
            unsafe {
                std::env::remove_var(self.key);
            }
        }
    }
}
