use crate::cli::{
    actions::{
        execute_fallback_command, execute_fallback_http, metrics::ServiceMetrics,
        should_continue_fallback,
    },
    config::{BodyType, ServiceDetails},
};
use anyhow::{Result, anyhow};
use futures_util::StreamExt;
use regex::Regex;
use reqwest::{Client, Method, RequestBuilder};
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
use tracing::{debug, error, info};

use std::hash::BuildHasher;

/// Builds a `reqwest::RequestBuilder` from the service details.
///
/// # Errors
///
/// Returns an error if the URL is missing, the method is invalid, or the request cannot be built.
pub fn build_http_request(
    client: &Client,
    service_details: &ServiceDetails,
) -> Result<RequestBuilder> {
    let url = service_details
        .url
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("No URL provided"))?;

    let method = Method::from_bytes(service_details.method.to_string().as_bytes())?;

    let mut request = client.request(method, url);

    if let Some(body) = &service_details.body {
        debug!("Building HTTP request with body: {:?}", body);

        match body {
            BodyType::Json(json) => {
                request = request.json(json);
            }
            BodyType::Form(form_data) => {
                request = request.form(form_data);
            }
            BodyType::Text(text) => {
                request = request.body(text.clone()); // Handles XML, plain text, etc.
            }
        }
    }

    Ok(request)
}

/// Handles the HTTP response
///
/// # Errors
///
/// Returns an error if the fallback command or HTTP request fails.
pub async fn handle_http_response<S: BuildHasher>(
    service_name: &str,
    service_details: &ServiceDetails,
    response: reqwest::Response,
    metrics: &ServiceMetrics,
    counters: Arc<Mutex<HashMap<String, usize, S>>>,
) -> Result<bool> {
    let status = response.status();
    let headers = response.headers().clone();

    // Check if the response status matches expected status
    let status_matches = status.as_u16() == service_details.expect.status;

    // Check if the response body matches expected criteria
    let body_matches = if let Some(expected_body) = &service_details.expect.body {
        let regex = generate_regex_pattern(expected_body).map_err(|e| {
            error!(
                "Invalid regex pattern in Expect body: {}, Error: {}",
                expected_body, e
            );
            e
        })?;

        let mut buffer = String::new(); // Accumulate response body here
        let mut stream = response.bytes_stream(); // Stream of response body bytes

        // Get max_bytes limit if set; otherwise, allow unlimited size
        let max_bytes = service_details.max_bytes.unwrap_or(usize::MAX); // Default to no limit
        let mut total_bytes_read = 0; // keep track of total bytes read

        while let Some(chunk) = stream.next().await {
            match chunk {
                Ok(bytes) => {
                    let chunk_size = bytes.len();
                    let remaining_bytes = max_bytes.saturating_sub(total_bytes_read);

                    debug!(
                        "Read chunk of size: {}, remaining bytes: {}, total bytes read: {}",
                        chunk_size, remaining_bytes, total_bytes_read
                    );

                    if remaining_bytes == 0 {
                        break; // Stop reading if max_bytes reached
                    }

                    // Truncate the chunk if it exceeds remaining bytes limit
                    let limited_chunk = if chunk_size > remaining_bytes {
                        bytes.get(..remaining_bytes).unwrap_or(&bytes)
                    } else {
                        &bytes
                    };

                    // Convert bytes to string and append to buffer
                    if let Ok(text) = std::str::from_utf8(limited_chunk) {
                        buffer.push_str(text);
                    }

                    // Update total bytes read
                    total_bytes_read += limited_chunk.len();

                    // Check regex on accumulated buffer
                    if regex.is_match(&buffer) {
                        debug!(
                            "Match found in response body: {}, total bytes read: {}",
                            buffer, total_bytes_read
                        );
                        return Ok(true); // Match found, stop reading
                    }

                    // Stop reading if max_bytes is reached
                    if total_bytes_read >= max_bytes {
                        debug!(
                            "Max bytes limit reached: {}, total bytes read: {}, body: {}",
                            max_bytes, total_bytes_read, buffer
                        );
                        break; // Stop reading if max_bytes reached
                    }
                }
                Err(e) => {
                    error!("Failed to read chunk: {}", e);
                    return Ok(false);
                }
            }
        }

        // No match found
        false
    } else {
        // No expected body means body check always passes
        true
    };

    let is_match = status_matches && body_matches;

    // Update metrics
    // Set service status to OK (1) if both status and body match
    metrics
        .epazote_status
        .with_label_values(&[service_name])
        .set(i64::from(is_match));

    info!(
        service_name = service_name,
        service_url = service_details.url,
        service_status = status.as_u16(),
        expected_status = service_details.expect.status,
        response_headers = ?headers,
        matches = is_match
    );

    if !is_match
        && let Some(action) = &service_details.expect.if_not
        && should_continue_fallback(service_name, &counters, action).await
    {
        if let Some(cmd) = &action.cmd {
            let exit_code = execute_fallback_command(cmd).await?;
            info!(
                "Executed fallback command for {} with exit code {}",
                service_name, exit_code
            );
        }

        if let Some(http) = &action.http {
            let status = execute_fallback_http(http).await?;
            info!(
                "Executed fallback HTTP request for {} with status code {}",
                service_name, status
            );
        }
    }

    Ok(is_match)
}

// Generates a regex pattern from the input string.
/// - If input starts with `r"`, extract and use it as a raw regex (strip `r"` and trailing `"` if present).
/// - Trims input before processing to remove extra whitespace.
fn generate_regex_pattern(input: &str) -> Result<Regex> {
    let trimmed_input = input.trim();

    if trimmed_input.is_empty() {
        return Err(anyhow!("Input regex pattern cannot be empty"));
    }

    let pattern = trimmed_input.strip_prefix("r\"").map_or_else(
        // Escape the input to prevent regex injection
        || format!(r".*{}.*", regex::escape(trimmed_input)),
        // If prefix exists, strip suffix and use raw regex
        |raw| raw.strip_suffix('"').unwrap_or(raw).to_string(),
    );

    debug!(
        "Generated regex for: {}, pattern: {}",
        trimmed_input, pattern
    );

    Regex::new(&pattern).map_err(|e| {
        debug!("Regex compilation failed: {}", e);
        e.into()
    })
}

#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
    use super::*;
    use crate::cli::{
        actions::client::build_client,
        config::{Config, Expect, HttpMethod, ServiceDetails},
    };
    use mockito::Server;
    use reqwest::StatusCode;
    use serde_json::json;
    use std::{io::Write, sync::Arc};
    use tokio::time::Duration;

    // Helper to create config from YAML
    fn create_config(yaml: &str) -> Config {
        let mut tmp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
        tmp_file
            .write_all(yaml.as_bytes())
            .expect("Failed to write to temp file");
        tmp_file.flush().expect("Failed to flush temp file");
        Config::new(tmp_file.path().to_path_buf()).expect("Failed to load config")
    }

    // helper to generate a string of numbers
    fn generate_numbers(limit: usize, start: usize) -> String {
        use std::fmt::Write;
        let mut result = String::new();
        let mut num = start;
        while result.len() + 2 < limit {
            // Approximate space for "N "
            let _ = write!(result, "{num} ");
            num += 1;
        }
        result
    }

    #[test]
    fn test_generate_regex_pattern() {
        // Normal input should be escaped and wrapped in ".* .*"
        let pattern = generate_regex_pattern("test").expect("Failed to generate regex pattern");
        assert_eq!(pattern.as_str(), r".*test.*");

        // Raw regex should be extracted without modification
        let pattern =
            generate_regex_pattern(r#"r"test""#).expect("Failed to generate regex pattern");
        assert_eq!(pattern.as_str(), "test");

        // Raw regex without closing quote should still work
        let pattern =
            generate_regex_pattern(r#"r"test"#).expect("Failed to generate regex pattern");
        assert_eq!(pattern.as_str(), "test");

        // Raw regex with extra quotes should be handled
        let pattern = generate_regex_pattern(r#"r"(?i).*hello.*""#)
            .expect("Failed to generate regex pattern");
        assert_eq!(pattern.as_str(), "(?i).*hello.*");

        // Standard input should be escaped
        let pattern =
            generate_regex_pattern("hello world").expect("Failed to generate regex pattern");
        assert_eq!(pattern.as_str(), r".*hello world.*"); // Space should be escaped
        //
        // Ensure regex matching works
        assert!(pattern.is_match("this is a hello world test"));
        assert!(!pattern.is_match("this is a goodbye test"));

        // the . should be escaped
        let pattern =
            generate_regex_pattern("hello.world").expect("Failed to generate regex pattern");
        assert_eq!(pattern.as_str(), r".*hello\.world.*"); // Space should be escaped
        //
        let pattern = generate_regex_pattern("a+b*").expect("Failed to generate regex pattern");
        assert_eq!(pattern.as_str(), r".*a\+b\*.*"); // Space should be escaped
    }

    #[tokio::test]
    async fn test_handle_http_response() {
        let mut server = Server::new_async().await;
        let _m = server
            .mock("GET", "/health")
            .with_status(200)
            .create_async()
            .await;

        let service = ServiceDetails {
            every: Duration::from_secs(1),
            expect: Expect {
                status: 200,
                header: None,
                body: None,
                if_not: None,
            },
            follow_redirects: Some(true),
            headers: None,
            max_bytes: None,
            test: None,
            timeout: Duration::from_secs(5),
            url: Some(format!("{}/health", server.url())),
            method: HttpMethod::Get,
            body: None,
        };

        let metrics = Arc::new(ServiceMetrics::new().expect("Failed to create metrics"));
        let counters: Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));

        let (builder, _client_config) =
            build_client(&service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, &service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        let rs = handle_http_response("test", &service, response, &metrics, counters).await;

        assert!(rs.is_ok());
    }

    #[tokio::test]
    async fn test_build_http_request_json() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r"
---
services:
  test:
    url: {mock_url}/test
    method: POST
    body:
      json:
        key: value
        oi: hola
    every: 30s
    headers:
      X-Custom-Header: TestValue
    expect:
      status: 200
    "
        );

        let config = create_config(&yaml);
        let service = config.services.get("test").expect("Service not found");

        // Define expected JSON body
        let expected_json = json!({
            "key": "value",
            "oi": "hola"
        });

        let _ = env_logger::try_init();
        let _mock = server
            .mock("POST", "/test")
            .match_header("X-Custom-Header", "TestValue")
            .match_header("Content-Type", "application/json")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .match_body(mockito::Matcher::Json(expected_json.clone()))
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");

        if let Some(body) = &config.services.get("test").expect("Service not found").body {
            let json_body = serde_json::to_string(body).expect("Failed to serialize body");
            assert_eq!(json_body, expected_json.to_string());
        }

        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[tokio::test]
    async fn test_build_http_request_form() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r"
---
services:
  test:
    url: {mock_url}/test
    method: POST
    body:
      form:
        key: value
        oi: hola
    every: 30s
    headers:
      X-Custom-Header: TestValue
    expect:
      status: 200
    "
        );

        let config = create_config(&yaml);
        let service = config.services.get("test").expect("Service not found");

        // Define expected form body
        let expected_form = [
            ("key".to_string(), "value".to_string()),
            ("oi".to_string(), "hola".to_string()),
        ];

        let _ = env_logger::try_init();
        let _mock = server
            .mock("POST", "/test")
            .match_header("X-Custom-Header", "TestValue")
            .match_header("Content-Type", "application/x-www-form-urlencoded")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .match_body(mockito::Matcher::UrlEncoded(
                "key".to_string(),
                "value".to_string(),
            ))
            .match_body(mockito::Matcher::UrlEncoded(
                "oi".to_string(),
                "hola".to_string(),
            ))
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");

        // Check that the body is correctly interpreted as a form
        if let Some(BodyType::Form(body)) =
            &config.services.get("test").expect("Service not found").body
        {
            for (key, value) in &expected_form {
                assert_eq!(body.get(key), Some(value));
            }
        } else {
            panic!("Expected BodyType::Form but found something else");
        }

        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[tokio::test]
    async fn test_build_http_request_text() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r#"
---
services:
  test:
    url: {mock_url}/test
    method: POST
    body: "Hello, world!"
    every: 30s
    headers:
      content-type: text/plain
      X-Custom-Header: TestValue
    expect:
      status: 200
    "#
        );

        let config = create_config(&yaml);
        let service = config.services.get("test").expect("Service not found");

        // Expected plain text body
        let expected_text = String::from("Hello, world!");

        let _ = env_logger::try_init();
        let _mock = server
            .mock("POST", "/test")
            .match_header("X-Custom-Header", "TestValue")
            .match_header("Content-Type", "text/plain")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .match_body(mockito::Matcher::Exact(expected_text.clone()))
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");

        // Check that the body is correctly interpreted as Text
        if let Some(BodyType::Text(body)) =
            &config.services.get("test").expect("Service not found").body
        {
            assert_eq!(body, &expected_text);
        } else {
            panic!("Expected BodyType::Text but found something else");
        }

        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[tokio::test]
    async fn test_handle_http_response_expect_body() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r"
---
services:
  test:
    url: {mock_url}/test
    every: 30s
    expect:
      status: 200
      body: sopas
    "
        );

        let config = create_config(&yaml);
        let service = config.services.get("test").expect("Service not found");

        let _ = env_logger::try_init();
        let _mock = server
            .mock("GET", "/test")
            .with_body("world-sopas-hello")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");

        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        let counters: Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));

        let rs = handle_http_response(
            "test",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            counters,
        )
        .await
        .expect("Failed to handle response");

        assert!(rs);
    }

    #[tokio::test]
    async fn test_handle_http_response_expect_body_regex_stop() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r#"
---
services:
  test-stop:
    url: {mock_url}/test
    every: 30s
    expect:
      status: 200
      body: r"\b(?:sopas|cit-02)\b" # match sopas or cit-02
      if_not:
        stop: 2
    "#
        );

        let config = create_config(&yaml);
        let service = config.services.get("test-stop").expect("Service not found");

        let _ = env_logger::try_init();
        let _mock = server
            .mock("GET", "/test")
            .with_body("---")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        let counters: Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));

        let rs1 = handle_http_response(
            "test-stop",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(!rs1);

        // Check counter after first attempt
        let count1 = {
            let counters_locked = counters.lock().await;
            *counters_locked.get("test-stop").unwrap_or(&0)
        };
        assert_eq!(count1, 1, "Counter should be 1 after first attempt");

        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        let rs2 = handle_http_response(
            "test-stop",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(!rs2);

        // Check counter after first attempt
        let count2 = {
            let counters_locked = counters.lock().await;
            *counters_locked.get("test-stop").unwrap_or(&0)
        };
        assert_eq!(count2, 2, "Counter should be 1 after first attempt");
    }

    #[tokio::test]
    async fn test_handle_http_response_expect_if_not_http() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r"
---
services:
  test-stop:
    url: {mock_url}/test
    every: 30s
    expect:
      status: 200
      body: http
      if_not:
        stop: 2
        http: {mock_url}/notify?milei=libra
    "
        );

        let config = create_config(&yaml);
        let service = config.services.get("test-stop").expect("Service not found");

        let _ = env_logger::try_init();
        let _mock = server
            .mock("GET", "/test")
            .with_body("---milei---")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        let counters: Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));

        let rs1 = handle_http_response(
            "test-stop",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(!rs1);

        // Check counter after first attempt
        let count1 = {
            let counters_locked = counters.lock().await;
            *counters_locked.get("test-stop").unwrap_or(&0)
        };
        assert_eq!(count1, 1, "Counter should be 1 after first attempt");

        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        let rs2 = handle_http_response(
            "test-stop",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(!rs2);

        // Check counter after first attempt
        let count2 = {
            let counters_locked = counters.lock().await;
            *counters_locked.get("test-stop").unwrap_or(&0)
        };
        assert_eq!(count2, 2, "Counter should be 1 after first attempt");
    }

    #[tokio::test]
    async fn test_handle_http_response_expect_body_regex_example() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r#"
---
services:
  test-stop:
    url: {mock_url}/test
    every: 30s
    expect:
      status: 200
      body: r"success|ok"
    "#
        );

        let config = create_config(&yaml);
        let service = config.services.get("test-stop").expect("Service not found");

        let _ = env_logger::try_init();
        let mock = server
            .mock("GET", "/test")
            .with_body("success")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        let counters: Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));

        let rs1 = handle_http_response(
            "test-stop",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(rs1);

        mock.remove();
        let _mock = server
            .mock("GET", "/test")
            .with_body("-- error --")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .with_status(200)
            .create_async()
            .await;

        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        let rs2 = handle_http_response(
            "test-stop",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(!rs2);

        mock.remove();
        let _mock = server
            .mock("GET", "/test")
            .with_body("-- ok --")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .with_status(200)
            .create_async()
            .await;

        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");

        let rs3 = handle_http_response(
            "test-stop",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(rs3);
    }

    #[tokio::test]
    async fn test_handle_http_response_expect_body_max_bytes_20() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r#"
---
services:
  test-max_bytes:
    url: {mock_url}/test
    every: 30s
    expect:
      status: 200
      body: "34917f37-72b9-403f-887c-20c5e93b7173"
    max_bytes: 20
    "#
        );

        let config = create_config(&yaml);
        let service = config
            .services
            .get("test-max_bytes")
            .expect("Service not found");

        let _ = env_logger::try_init();
        let _mock = server
            .mock("GET", "/test")
            .with_body("hello world 0123456789 34917f37-72b9-403f-887c-20c5e93b7173")
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");
        let counters: Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));

        // print body
        let rs = handle_http_response(
            "test-max_bytes",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(!rs);
    }

    #[tokio::test]
    async fn test_handle_http_response_expect_body_max_bytes_64k() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r#"
---
services:
  test-max_bytes:
    url: {mock_url}/test
    every: 30s
    expect:
      status: 200
      body: "34917f37-72b9-403f-887c-20c5e93b7173"
    max_bytes: 64000
    "#
        );

        let response_body = format!(
            "{}{} --- FIN",
            generate_numbers(64 * 1024, 0),
            "34917f37-72b9-403f-887c-20c5e93b7173"
        );

        let config = create_config(&yaml);
        let service = config
            .services
            .get("test-max_bytes")
            .expect("Service not found");

        let _ = env_logger::try_init();
        let _mock = server
            .mock("GET", "/test")
            .with_body(response_body)
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");
        let counters: Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));

        // print body
        let rs = handle_http_response(
            "test-max_bytes",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(!rs);
    }

    #[tokio::test]
    async fn test_handle_http_response_expect_body_read_in_chunks() {
        // Start mock server
        let mut server = Server::new_async().await;
        let mock_url = server.url();

        let yaml = format!(
            r#"
---
services:
  test-max_bytes:
    url: {mock_url}/test
    every: 30s
    expect:
      status: 200
      body: "34917f37-72b9-403f-887c-20c5e93b7173"
    "#
        );

        let response_body = format!(
            "{} --- {} --- {} --- FIN",
            generate_numbers(1024, 0),
            "34917f37-72b9-403f-887c-20c5e93b7173",
            generate_numbers(128 * 1024, 0)
        );

        let config = create_config(&yaml);
        let service = config
            .services
            .get("test-max_bytes")
            .expect("Service not found");

        let _ = env_logger::try_init();
        let _mock = server
            .mock("GET", "/test")
            .with_body(response_body)
            .match_header(
                "User-Agent",
                mockito::Matcher::Regex("epazote.*".to_string()),
            )
            .with_status(200)
            .create_async()
            .await;

        let (builder, _client_config) =
            build_client(service).expect("Failed to build client builder");
        let client = builder.build().expect("Failed to build client");
        let request = build_http_request(&client, service).expect("Failed to build request");
        let response = client
            .execute(request.build().expect("Failed to build request"))
            .await
            .expect("Failed to execute request");
        let counters: Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));

        // print body
        let rs = handle_http_response(
            "test-max_bytes",
            service,
            response,
            &ServiceMetrics::new().expect("Failed to create metrics"),
            Arc::clone(&counters),
        )
        .await
        .expect("Failed to handle response");

        assert!(rs);
    }
}
