Documentation
// Copyright (c) 2025, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

//! HTTP testing service for integration testing
//!
//! This module provides predefined types and configurations for HTTPBin services
//! in integration tests. HTTPBin is a simple HTTP request and response service
//! useful for testing HTTP clients and APIs.
//!

use std::time::Duration;

use crate::config::{Config, ContainerConfig};
use crate::container::Container;
use crate::error::TestError;
use crate::image::Image;
use crate::port::{Port, PortAccess, PortVisibility};
use crate::probe::{MessageProbe, MessageSource};
use crate::service::Service;

const PORT: Port = 80;
const SCHEMA: &str = "http";
const HTTPBIN_IMAGE_NAME: &str = "kennethreitz/httpbin";

/// Struct to manage the HTTPBin configuration.
#[derive(Clone)]
pub struct HttpBinConfig {
    hostname: String,
    version: String,
    image_name: String,
    visibility: PortVisibility,
    timeout: Duration,
}

impl HttpBinConfig {
    /// Get the hostname of the HTTPBin configuration.
    pub fn hostname(&self) -> &str {
        &self.hostname
    }

    /// Get the version of the HTTPBin configuration.
    pub fn version(&self) -> &str {
        &self.version
    }

    /// Get the image name of the HTTPBin configuration.
    pub fn image_name(&self) -> &str {
        &self.image_name
    }

    /// Get the visibility of the HTTPBin configuration.
    pub fn visibility(&self) -> PortVisibility {
        self.visibility
    }

    /// Get the timeout of the HTTPBin configuration.
    pub fn timeout(&self) -> Duration {
        self.timeout
    }

    /// Create a new HTTPBin configuration.
    pub fn new() -> Self {
        Self {
            hostname: "backend".to_string(),
            version: "latest".to_string(),
            image_name: HTTPBIN_IMAGE_NAME.to_string(),
            timeout: Duration::from_secs(30),
            visibility: Default::default(),
        }
    }

    /// Create a new HTTPBin configuration builder.
    pub fn builder() -> HttpBinConfigBuilder {
        HttpBinConfigBuilder::new()
    }
}

/// Builder for [`HttpBinConfig`].
pub struct HttpBinConfigBuilder {
    config: HttpBinConfig,
}

impl HttpBinConfigBuilder {
    fn new() -> Self {
        Self {
            config: HttpBinConfig::new(),
        }
    }

    /// Set the hostname of the HTTPBin configuration.
    pub fn hostname<T: Into<String>>(self, hostname: T) -> Self {
        Self {
            config: HttpBinConfig {
                hostname: hostname.into(),
                ..self.config
            },
        }
    }

    /// Set the version of the HTTPBin configuration.
    pub fn version<T: Into<String>>(self, version: T) -> Self {
        Self {
            config: HttpBinConfig {
                version: version.into(),
                ..self.config
            },
        }
    }

    /// Set the image name of the HTTPBin configuration.
    pub fn image_name<T: Into<String>>(self, image_name: T) -> Self {
        Self {
            config: HttpBinConfig {
                image_name: image_name.into(),
                ..self.config
            },
        }
    }

    /// Set the visibility of the HTTPBin configuration.
    pub fn visibility(self, visibility: PortVisibility) -> Self {
        Self {
            config: HttpBinConfig {
                visibility,
                ..self.config
            },
        }
    }

    /// Set the timeout of the HTTPBin configuration.
    pub fn timeout(self, timeout: Duration) -> Self {
        Self {
            config: HttpBinConfig {
                timeout,
                ..self.config
            },
        }
    }

    /// Build the HTTPBin configuration.
    pub fn build(self) -> HttpBinConfig {
        self.config
    }
}

impl Default for HttpBinConfig {
    fn default() -> Self {
        Self::new()
    }
}

impl Config for HttpBinConfig {
    fn hostname(&self) -> &str {
        &self.hostname
    }

    fn port(&self) -> Port {
        PORT
    }

    fn schema(&self) -> &str {
        SCHEMA
    }

    fn to_container_config(&self) -> Result<ContainerConfig, TestError> {
        Ok(ContainerConfig::builder(
            self.hostname.clone(),
            Image::from_repository(self.image_name()).with_version(self.version()),
        )
        .ports([PortAccess::new(PORT, self.visibility)])
        .readiness(
            MessageProbe::builder("Using worker: gevent")
                .timeout(Duration::from_secs(30))
                .source(MessageSource::StdErr)
                .build(),
        )
        .build())
    }
}

/// Represents a `HttpBin` service instance.
pub struct HttpBin {
    socket: Option<String>,
    address: Option<String>,
}

impl HttpBin {
    /// Returns the socket for configuring the HttpBin service.
    pub fn socket(&self) -> Option<&str> {
        self.socket.as_deref()
    }

    /// Returns the address for configuring the HttpBin service.
    pub fn address(&self) -> Option<&str> {
        self.address.as_deref()
    }
}

impl Service for HttpBin {
    type Config = HttpBinConfig;

    fn new(_config: &Self::Config, container: &Container) -> Self {
        let socket = container.socket(PORT).map(String::from);
        let address = socket.as_ref().map(|s| format!("http://{s}"));
        Self { socket, address }
    }
}