use std::collections::HashMap;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex, OnceLock};

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::task::JoinHandle;

pub struct StubResponse {
    status: &'static str,
    headers: Vec<(String, String)>,
    body: String,
}

impl StubResponse {
    pub fn json(body: &str) -> Self {
        Self {
            status: "200 OK",
            headers: vec![("content-type".to_string(), "application/json".to_string())],
            body: body.to_string(),
        }
    }

    pub fn not_found() -> Self {
        Self {
            status: "404 Not Found",
            headers: Vec::new(),
            body: String::new(),
        }
    }

    pub fn with_header(mut self, name: &str, value: &str) -> Self {
        self.headers.push((name.to_string(), value.to_string()));
        self
    }

    pub fn with_status(mut self, status: &'static str) -> Self {
        self.status = status;
        self
    }
}

pub struct TestServer {
    base_url: String,
    handle: JoinHandle<()>,
    requests: Arc<Mutex<Vec<String>>>,
}

impl TestServer {
    pub fn url(&self) -> &str {
        &self.base_url
    }

    pub fn requests(&self) -> Vec<String> {
        self.requests.lock().unwrap().clone()
    }
}

impl Drop for TestServer {
    fn drop(&mut self) {
        self.handle.abort();
    }
}

pub fn env_lock() -> &'static Mutex<()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
}

fn env_overrides() -> &'static Mutex<HashMap<String, Option<String>>> {
    static OVERRIDES: OnceLock<Mutex<HashMap<String, Option<String>>>> = OnceLock::new();
    OVERRIDES.get_or_init(|| Mutex::new(HashMap::new()))
}

pub fn set_env_var(key: &str, value: &str) {
    env_overrides()
        .lock()
        .unwrap()
        .insert(key.to_string(), Some(value.to_string()));
}

pub fn remove_env_var(key: &str) {
    env_overrides()
        .lock()
        .unwrap()
        .insert(key.to_string(), None);
}

pub fn read_env_var(key: &str) -> Option<String> {
    env_overrides().lock().unwrap().get(key).cloned().flatten()
}

pub async fn spawn_server(responses: Vec<(String, StubResponse)>) -> TestServer {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let address = listener.local_addr().unwrap();
    let base_url = format!("http://{address}");
    let requests = Arc::new(Mutex::new(Vec::new()));
    let requests_for_task = Arc::clone(&requests);
    let mut responses = VecDeque::from(responses);
    let base_url_for_task = base_url.clone();

    let handle = tokio::spawn(async move {
        while let Some((expected_path, response)) = responses.pop_front() {
            let (mut stream, _) = listener.accept().await.unwrap();
            let mut buffer = vec![0; 8192];
            let bytes_read = stream.read(&mut buffer).await.unwrap();
            let request = String::from_utf8_lossy(&buffer[..bytes_read]);
            let request_line = request.lines().next().unwrap_or_default();
            let request_path = request_line
                .split_whitespace()
                .nth(1)
                .unwrap_or_default()
                .to_string();

            requests_for_task.lock().unwrap().push(request_path.clone());
            assert_eq!(request_path, expected_path, "unexpected request path");

            let body = response.body.replace("{{base_url}}", &base_url_for_task);
            let mut response_text = format!(
                "HTTP/1.1 {}\r\ncontent-length: {}\r\nconnection: close\r\n",
                response.status,
                body.len()
            );
            for (name, value) in response.headers {
                response_text.push_str(&format!(
                    "{name}: {}\r\n",
                    value.replace("{{base_url}}", &base_url_for_task)
                ));
            }
            response_text.push_str("\r\n");
            response_text.push_str(&body);

            stream.write_all(response_text.as_bytes()).await.unwrap();
        }
    });

    TestServer {
        base_url,
        handle,
        requests,
    }
}
