use std::env;
use std::sync::Once;
use std::time::Duration;

use anyhow::{ensure, Result};
use obws::{
    requests::SceneItem,
    responses::{Output, Profile, Scene, SceneCollection, SourceListItem, Transition},
    Client,
};
use tokio::time;

pub const TEST_OUTPUT: &str = "virtualcam_output";
pub const TEST_COLLECTION: &str = "OBWS-TEST";
pub const TEST_PROFILE: &str = "OBWS-TEST";
pub const TEST_SCENE: &str = "OBWS-TEST-Scene";
pub const TEST_SCENE_2: &str = "OBWS-TEST-Scene2";
pub const TEXT_SOURCE: &str = "OBWS-TEST-Text";
pub const TEXT_SOURCE_2: &str = "OBWS-TEST-Text2";
pub const TEST_TRANSITION: &str = "OBWS-TEST-Transition";
pub const TEST_TRANSITION_2: &str = "OBWS-TEST-Transition2";
pub const TEST_BROWSER: &str = "OBWS-TEST-Browser";
pub const TEST_MEDIA: &str = "OBWS-TEST-Media";
pub const SOURCE_KIND_TEXT_FT2: &str = "text_ft2_source_v2";
pub const SOURCE_KIND_BROWSER: &str = "browser_source";
pub const SOURCE_KIND_VLC: &str = "vlc_source";

const SCENE_ORDER: &[SceneItem] = &[
    SceneItem {
        id: None,
        name: Some(TEXT_SOURCE),
    },
    SceneItem {
        id: None,
        name: Some(TEXT_SOURCE_2),
    },
    SceneItem {
        id: None,
        name: Some(TEST_BROWSER),
    },
    SceneItem {
        id: None,
        name: Some(TEST_MEDIA),
    },
];

static INIT: Once = Once::new();

pub async fn new_client() -> Result<Client> {
    INIT.call_once(|| {
        if matches!(env::var("USER").as_deref(), Ok("vagrant")) {
            env::set_var("OBS_HOST", "192.168.10.1");
        }

        dotenv::dotenv().ok();
        pretty_env_logger::init();
    });

    let host = env::var("OBS_HOST").unwrap_or_else(|_| "localhost".to_owned());
    let client = Client::connect(host, 4444).await?;
    client.login(env::var("OBS_PASSWORD").ok()).await?;

    let collections = client.scene_collections().list_scene_collections().await?;
    ensure!(
        collections.iter().any(is_required_scene_collection),
        "scene collection `{}` not found, required for all tests",
        TEST_COLLECTION
    );

    client
        .scene_collections()
        .set_current_scene_collection("OBWS-TEST")
        .await?;

    // Give OBS some time to load the scene collection
    time::sleep(Duration::from_secs(1)).await;

    ensure_obs_setup(&client).await?;

    Ok(client)
}

async fn ensure_obs_setup(client: &Client) -> Result<()> {
    let outputs = client.outputs().list_outputs().await?;
    ensure!(
        outputs.iter().any(is_required_output),
        "output `{}` not found, required for output tests",
        TEST_OUTPUT
    );

    let scenes = client.scenes().get_scene_list().await?;
    ensure!(
        scenes.scenes.iter().any(is_required_scene),
        "scene `{}` not found, required for scenes tests",
        TEST_SCENE
    );
    ensure!(
        scenes.scenes.iter().any(is_required_scene_2),
        "scene `{}` not found, required for scenes tests",
        TEST_SCENE
    );

    let sources = client.sources().get_sources_list().await?;
    ensure!(
        sources.iter().any(is_required_source),
        "text source `{}` not found, required for sources tests",
        TEXT_SOURCE
    );
    ensure!(
        sources.iter().any(is_required_source_2),
        "text source `{}` not found, required for sources tests",
        TEXT_SOURCE
    );
    ensure!(
        sources.iter().any(is_required_browser_source),
        "media source `{}` not found, required for sources tests",
        TEST_BROWSER
    );
    ensure!(
        sources.iter().any(is_required_media_source),
        "media source `{}` not found, required for media control tests",
        TEST_MEDIA
    );

    let special_sources = client.sources().get_special_sources().await?;
    ensure!(
        special_sources.desktop_1.is_some(),
        "desktop audio device required for sources tests"
    );

    let profiles = client.profiles().list_profiles().await?;
    ensure!(
        profiles.iter().any(is_required_profile),
        "profile `{}` not found, required for profiles tests",
        TEST_PROFILE
    );

    let studio_mode_enabled = client.studio_mode().get_studio_mode_status().await?;
    ensure!(
        !studio_mode_enabled,
        "studio mode enabled, required to be disabled for studio mode tests"
    );

    let transitions = client.transitions().get_transition_list().await?;
    ensure!(
        transitions.transitions.iter().any(is_required_transition),
        "transition `{}` not found, required for transitions tests",
        TEST_TRANSITION
    );
    ensure!(
        transitions.transitions.iter().any(is_required_transition_2),
        "transition `{}` not found, required for transitions tests",
        TEST_TRANSITION
    );

    client.scenes().set_current_scene(TEST_SCENE).await?;
    client
        .scenes()
        .reorder_scene_items(Some(TEST_SCENE), SCENE_ORDER)
        .await?;
    client
        .transitions()
        .set_current_transition(TEST_TRANSITION)
        .await?;

    Ok(())
}

fn is_required_output(output: &Output) -> bool {
    output.name == TEST_OUTPUT
}

fn is_required_scene_collection(output: &SceneCollection) -> bool {
    output.sc_name == TEST_COLLECTION
}

fn is_required_scene(scene: &Scene) -> bool {
    scene.name == TEST_SCENE
}

fn is_required_scene_2(scene: &Scene) -> bool {
    scene.name == TEST_SCENE_2
}

fn is_required_source(source: &SourceListItem) -> bool {
    source.name == TEXT_SOURCE && is_text_input_source(source)
}

fn is_required_source_2(source: &SourceListItem) -> bool {
    source.name == TEXT_SOURCE_2 && is_text_input_source(source)
}

fn is_required_browser_source(source: &SourceListItem) -> bool {
    source.name == TEST_BROWSER && is_browser_input_source(source)
}

fn is_required_media_source(source: &SourceListItem) -> bool {
    source.name == TEST_MEDIA && is_media_input_source(source)
}

fn is_text_input_source(source: &SourceListItem) -> bool {
    source.ty == "input" && source.type_id == SOURCE_KIND_TEXT_FT2
}

fn is_browser_input_source(source: &SourceListItem) -> bool {
    source.ty == "input" && source.type_id == SOURCE_KIND_BROWSER
}

fn is_media_input_source(source: &SourceListItem) -> bool {
    source.ty == "input" && source.type_id == SOURCE_KIND_VLC
}

fn is_required_profile(profile: &Profile) -> bool {
    profile.profile_name == TEST_PROFILE
}

fn is_required_transition(transition: &Transition) -> bool {
    transition.name == TEST_TRANSITION
}

fn is_required_transition_2(transition: &Transition) -> bool {
    transition.name == TEST_TRANSITION_2
}

#[allow(unused_macros)]
macro_rules! wait_for {
    ($expression:expr, $pattern:pat) => {
        while let Some(Event { ty, .. }) = $expression.next().await {
            if matches!(ty, $pattern) {
                break;
            }
        }
    };
}
