mod agent;
mod auth;
mod embeddings;
mod extractor;
mod extractor_usage;
mod models;
mod multi_extract;
mod noninteractive_oauth_cassette;
mod permission_control;
mod reasoning_roundtrip;
mod reasoning_tool_roundtrip;
mod request_hook;
mod routing;
mod streaming;
mod streaming_tools;
mod structured_output;
mod typed_prompt_tools;

use assert_fs::TempDir;
use rig::providers::copilot;
use std::borrow::Cow;
use std::future::Future;
use std::panic::AssertUnwindSafe;

use crate::cassettes::{CassetteSpec, ProviderCassette};
use futures::FutureExt;

pub(crate) const LIVE_MODEL: &str = copilot::GPT_4O;
pub(crate) const LIVE_LIGHT_MODEL: &str = copilot::GPT_4O_MINI;

fn first_env_value(keys: &[&str]) -> Option<String> {
    keys.iter().find_map(|name| {
        std::env::var(name)
            .ok()
            .filter(|value| !value.trim().is_empty())
    })
}

pub(crate) fn copilot_api_key() -> Option<String> {
    first_env_value(&["GITHUB_COPILOT_API_KEY", "COPILOT_API_KEY"])
}

pub(crate) fn copilot_github_access_token() -> Option<String> {
    first_env_value(&["COPILOT_GITHUB_ACCESS_TOKEN", "GITHUB_TOKEN"])
}

pub(crate) fn live_responses_model() -> Cow<'static, str> {
    first_env_value(&["GITHUB_COPILOT_RESPONSES_MODEL", "COPILOT_RESPONSES_MODEL"])
        .map(Cow::Owned)
        .unwrap_or_else(|| Cow::Borrowed(copilot::GPT_5_3_CODEX))
}

pub(crate) fn live_embedding_model() -> Cow<'static, str> {
    first_env_value(&["GITHUB_COPILOT_EMBEDDING_MODEL", "COPILOT_EMBEDDING_MODEL"])
        .map(Cow::Owned)
        .unwrap_or_else(|| Cow::Borrowed(copilot::TEXT_EMBEDDING_3_SMALL))
}

fn env_base_url() -> Option<String> {
    first_env_value(&["GITHUB_COPILOT_API_BASE", "COPILOT_BASE_URL"])
}

fn cassette_base_url() -> String {
    env_base_url().unwrap_or_else(|| "https://api.githubcopilot.com".to_string())
}

fn with_base_url(mut builder: copilot::ClientBuilder) -> copilot::ClientBuilder {
    if let Some(base_url) = env_base_url() {
        builder = builder.base_url(base_url);
    }

    builder
}

pub(crate) fn api_key_builder(api_key: impl Into<String>) -> copilot::ClientBuilder {
    with_base_url(copilot::Client::builder().api_key(api_key.into()))
}

pub(crate) fn github_access_token_builder(
    access_token: impl Into<String>,
) -> copilot::ClientBuilder {
    with_base_url(copilot::Client::builder().github_access_token(access_token.into()))
}

pub(crate) fn oauth_builder() -> copilot::ClientBuilder {
    with_base_url(copilot::Client::builder().oauth())
}

pub(crate) fn live_builder() -> copilot::ClientBuilder {
    if let Some(api_key) = copilot_api_key() {
        api_key_builder(api_key)
    } else if let Some(access_token) = copilot_github_access_token() {
        github_access_token_builder(access_token)
    } else {
        oauth_builder()
    }
}

pub(crate) fn live_client() -> copilot::Client {
    live_builder().build().expect("Copilot client should build")
}

async fn copilot_cassette(spec: impl Into<CassetteSpec>) -> (ProviderCassette, copilot::Client) {
    let cassette_base_url = cassette_base_url();
    let cassette = ProviderCassette::start("copilot", spec, &cassette_base_url).await;
    let client = copilot::Client::builder()
        .api_key(cassette.api_key("GITHUB_COPILOT_API_KEY"))
        .base_url(cassette.base_url())
        .build()
        .expect("Copilot cassette client should build");

    (cassette, client)
}

async fn copilot_noninteractive_oauth_cassette(
    spec: impl Into<CassetteSpec>,
) -> (ProviderCassette, copilot::Client, TempDir) {
    let cassette_base_url = cassette_base_url();
    let cassette = ProviderCassette::start("copilot", spec, &cassette_base_url).await;
    let temp = TempDir::new().expect("temp token directory should be created");
    let api_key_record = serde_json::json!({
        "token": cassette.api_key("GITHUB_COPILOT_API_KEY"),
        "expires_at": i64::MAX,
    });
    std::fs::write(
        temp.path().join("api-key.json"),
        serde_json::to_vec_pretty(&api_key_record).expect("api key record should serialize"),
    )
    .expect("api key record should be written");

    let client = copilot::Client::builder()
        .oauth()
        .allow_device_flow(false)
        .token_dir(temp.path())
        .base_url(cassette.base_url())
        .build()
        .expect("non-interactive Copilot OAuth cassette client should build");

    (cassette, client, temp)
}

pub(crate) async fn with_copilot_cassette<F, Fut>(spec: impl Into<CassetteSpec>, test_body: F)
where
    F: FnOnce(copilot::Client) -> Fut,
    Fut: Future<Output = ()>,
{
    let (cassette, client) = copilot_cassette(spec).await;
    let result = AssertUnwindSafe(test_body(client)).catch_unwind().await;
    cassette.finish_after_test(result).await;
}

pub(crate) async fn with_copilot_cassette_result<F, Fut, E>(
    spec: impl Into<CassetteSpec>,
    test_body: F,
) -> Result<(), E>
where
    F: FnOnce(copilot::Client) -> Fut,
    Fut: Future<Output = Result<(), E>>,
{
    let (cassette, client) = copilot_cassette(spec).await;
    let result = AssertUnwindSafe(test_body(client)).catch_unwind().await;
    cassette.finish_after_test_result(result).await
}

pub(crate) async fn with_copilot_noninteractive_oauth_cassette<F, Fut>(
    spec: impl Into<CassetteSpec>,
    test_body: F,
) where
    F: FnOnce(copilot::Client) -> Fut,
    Fut: Future<Output = ()>,
{
    let (cassette, client, _temp) = copilot_noninteractive_oauth_cassette(spec).await;
    let result = AssertUnwindSafe(test_body(client)).catch_unwind().await;
    cassette.finish_after_test(result).await;
}
