#![allow(clippy::unwrap_used)]

use crate::product::agent::features::Feature;
use crate::product::agent::protocol::SandboxPolicy;
use crate::product::protocol::config_types::WebSearchMode;
use crate::test_support::core::load_sse_fixture_with_id;
use crate::test_support::core::responses;
use crate::test_support::core::responses::start_mock_server;
use crate::test_support::core::skip_if_no_network;
use crate::test_support::core::test_codex::test_codex;
use lha_llm::built_in_runtime_endpoints;
use pretty_assertions::assert_eq;
use serde_json::Value;

fn sse_completed(id: &str) -> String {
    load_sse_fixture_with_id("../fixtures/completed_template.json", id)
}

#[allow(clippy::expect_used)]
fn find_web_search_tool(body: &Value) -> &Value {
    body["tools"]
        .as_array()
        .expect("request body should include tools array")
        .iter()
        .find(|tool| tool.get("type").and_then(Value::as_str) == Some("web_search"))
        .expect("tools should include a web_search tool")
}

#[allow(clippy::expect_used)]
fn has_web_search_tool(body: &Value) -> bool {
    body["tools"]
        .as_array()
        .expect("request body should include tools array")
        .iter()
        .any(|tool| tool.get("type").and_then(Value::as_str) == Some("web_search"))
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn web_search_mode_cached_sets_external_web_access_false() {
    skip_if_no_network!();

    let server = start_mock_server().await;
    let sse = sse_completed("resp-1");
    let resp_mock = responses::mount_sse_once(&server, sse).await;

    let mut builder = test_codex()
        .with_model("gpt-5-codex")
        .with_config(|config| {
            config.web_search_mode = Some(WebSearchMode::Cached);
        });
    let test = builder
        .build(&server)
        .await
        .expect("create test LHA conversation");

    test.submit_turn("hello cached web search")
        .await
        .expect("submit turn");

    let body = resp_mock.single_request().body_json();
    let tool = find_web_search_tool(&body);
    assert_eq!(
        tool.get("external_web_access").and_then(Value::as_bool),
        Some(false),
        "web_search cached mode should force external_web_access=false"
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn web_search_mode_takes_precedence_over_legacy_flags() {
    skip_if_no_network!();

    let server = start_mock_server().await;
    let sse = sse_completed("resp-1");
    let resp_mock = responses::mount_sse_once(&server, sse).await;

    let mut builder = test_codex()
        .with_model("gpt-5-codex")
        .with_config(|config| {
            config.features.enable(Feature::WebSearchRequest);
            config.web_search_mode = Some(WebSearchMode::Cached);
        });
    let test = builder
        .build(&server)
        .await
        .expect("create test LHA conversation");

    test.submit_turn("hello cached+live flags")
        .await
        .expect("submit turn");

    let body = resp_mock.single_request().body_json();
    let tool = find_web_search_tool(&body);
    assert_eq!(
        tool.get("external_web_access").and_then(Value::as_bool),
        Some(false),
        "web_search mode should win over legacy web_search_request"
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn web_search_mode_defaults_to_cached_when_unset() {
    skip_if_no_network!();

    let server = start_mock_server().await;
    let sse = sse_completed("resp-1");
    let resp_mock = responses::mount_sse_once(&server, sse).await;

    let mut builder = test_codex()
        .with_model("gpt-5-codex")
        .with_config(|config| {
            config.web_search_mode = None;
            config.features.disable(Feature::WebSearchCached);
            config.features.disable(Feature::WebSearchRequest);
        });
    let test = builder
        .build(&server)
        .await
        .expect("create test LHA conversation");

    test.submit_turn_with_policy("hello default cached web search", SandboxPolicy::ReadOnly)
        .await
        .expect("submit turn");

    let body = resp_mock.single_request().body_json();
    let tool = find_web_search_tool(&body);
    assert_eq!(
        tool.get("external_web_access").and_then(Value::as_bool),
        Some(false),
        "default web_search should be cached when unset"
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn web_search_mode_updates_between_turns_with_sandbox_policy() {
    skip_if_no_network!();

    let server = start_mock_server().await;
    let resp_mock = responses::mount_sse_sequence(
        &server,
        vec![sse_completed("resp-1"), sse_completed("resp-2")],
    )
    .await;

    let mut builder = test_codex()
        .with_model("gpt-5-codex")
        .with_config(|config| {
            config.web_search_mode = None;
            config.features.disable(Feature::WebSearchCached);
            config.features.disable(Feature::WebSearchRequest);
        });
    let test = builder
        .build(&server)
        .await
        .expect("create test LHA conversation");

    test.submit_turn_with_policy("hello cached", SandboxPolicy::ReadOnly)
        .await
        .expect("submit first turn");
    test.submit_turn_with_policy("hello live", SandboxPolicy::DangerFullAccess)
        .await
        .expect("submit second turn");

    let requests = resp_mock.requests();
    assert_eq!(requests.len(), 2, "expected two response requests");

    let first_body = requests[0].body_json();
    let first_tool = find_web_search_tool(&first_body);
    assert_eq!(
        first_tool
            .get("external_web_access")
            .and_then(Value::as_bool),
        Some(false),
        "read-only policy should default web_search to cached"
    );

    let second_body = requests[1].body_json();
    let second_tool = find_web_search_tool(&second_body);
    assert_eq!(
        second_tool
            .get("external_web_access")
            .and_then(Value::as_bool),
        Some(true),
        "danger-full-access policy should default web_search to live"
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn web_search_mode_defaults_to_disabled_for_azure_responses() {
    skip_if_no_network!();

    let server = start_mock_server().await;
    let sse = sse_completed("resp-1");
    let resp_mock = responses::mount_sse_once(&server, sse).await;

    let mut builder = test_codex()
        .with_model("gpt-5-codex")
        .with_config(|config| {
            let base_url = config.model_provider.base_url.clone();
            let mut provider = built_in_runtime_endpoints()["openai"].clone();
            provider.name = "Azure".to_string();
            provider.base_url = base_url;
            provider.set_response_turns();
            config.model_provider_id = provider.name.clone();
            config.model_provider = provider;
            config.web_search_mode = None;
            config.features.disable(Feature::WebSearchCached);
            config.features.disable(Feature::WebSearchRequest);
        });
    let test = builder
        .build(&server)
        .await
        .expect("create test LHA conversation");

    test.submit_turn_with_policy(
        "hello azure default web search",
        SandboxPolicy::DangerFullAccess,
    )
    .await
    .expect("submit turn");

    let body = resp_mock.single_request().body_json();
    assert_eq!(
        has_web_search_tool(&body),
        false,
        "azure responses requests should disable web_search by default"
    );
}
