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

//! HTTP mock service for integration testing
//!
//! This module provides predefined types and configurations for HTTP mock services
//! in integration tests. It supports creating mock HTTP endpoints for testing
//! API interactions and responses.
//!

use std::sync::OnceLock;
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};
use crate::portpicker;
use crate::probe::{MessageProbe, MessageSource};
use crate::service::Service;

/// Public port for HTTP Mock is chosen once and reused for the whole test suite
static HTTPMOCK_PUBLIC_PORT: OnceLock<Option<Port>> = OnceLock::new();

const SCHEMA: &str = "http";

/// Default Httpmock Docker image name.
pub const HTTPMOCK_IMAGE_NAME: &str = "alexliesenfeld/httpmock";

/// Configuration for a Httpmock service.
#[derive(Debug, Clone)]
pub struct HttpMockConfig {
    hostname: String,
    version: String,
    image_name: String,
    timeout: Duration,
    port: Port,
}

impl HttpMockConfig {
    /// Create a new Httpmock configuration.
    pub fn new() -> Self {
        Self {
            hostname: "backend".to_string(),
            version: "latest".to_string(),
            image_name: HTTPMOCK_IMAGE_NAME.to_string(),
            timeout: Duration::from_secs(30),
            port: 5000,
        }
    }
}

impl HttpMockConfig {
    /// Returns the Httpmock service hostname.
    pub fn hostname(&self) -> &str {
        &self.hostname
    }

    /// Returns the Httpmock Docker version.
    pub fn version(&self) -> &str {
        &self.version
    }

    /// Returns the Httpmock Docker image name.
    pub fn image_name(&self) -> &str {
        &self.image_name
    }

    /// Returns the Httpmock service readiness timeout.
    pub fn timeout(&self) -> Duration {
        self.timeout
    }

    /// Returns the Httpmock service configuration port.
    pub fn port(&self) -> Port {
        self.port
    }

    /// Returns a builder for [`HttpMockConfig`].
    pub fn builder() -> HttpMockConfigBuilder {
        HttpMockConfigBuilder::new()
    }
}

/// Builder for [`HttpMockConfig`].
#[derive(Debug, Clone)]
pub struct HttpMockConfigBuilder {
    config: HttpMockConfig,
}

impl HttpMockConfigBuilder {
    fn new() -> Self {
        Self {
            config: HttpMockConfig::new(),
        }
    }

    /// Sets the Httpmock service hostname.
    /// By default set to "backend".
    pub fn hostname<T: Into<String>>(self, hostname: T) -> Self {
        Self {
            config: HttpMockConfig {
                hostname: hostname.into(),
                ..self.config
            },
        }
    }

    /// Sets the Httpmock Docker version.
    /// By default set to "latest".
    pub fn version<T: Into<String>>(self, version: T) -> Self {
        Self {
            config: HttpMockConfig {
                version: version.into(),
                ..self.config
            },
        }
    }

    /// Sets the Httpmock Docker image name.
    /// By default set to [`HTTPMOCK_IMAGE_NAME`].
    pub fn image_name<T: Into<String>>(self, image_name: T) -> Self {
        Self {
            config: HttpMockConfig {
                image_name: image_name.into(),
                ..self.config
            },
        }
    }

    /// Sets the Httpmock configuration port.
    pub fn port(self, port: Port) -> Self {
        Self {
            config: HttpMockConfig {
                port,
                ..self.config
            },
        }
    }

    /// Sets the Httpmock service readiness timeout.
    pub fn timeout(self, timeout: Duration) -> Self {
        Self {
            config: HttpMockConfig {
                timeout,
                ..self.config
            },
        }
    }

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

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

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

    fn port(&self) -> Port {
        self.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::published_with_fixed_public_port(
            self.port,
            HTTPMOCK_PUBLIC_PORT
                .get_or_init(portpicker::pick_unused_port)
                .to_owned()
                .expect("No available ports for HTTP Mock container"),
        )])
        .env([("HTTPMOCK_PORT", self.port)])
        .readiness(
            MessageProbe::builder("Listening on")
                .timeout(self.timeout)
                .source(MessageSource::StdErr)
                .build(),
        )
        .build())
    }
}

/// Represents an Httpmock service instance.
#[derive(Debug)]
pub struct HttpMock {
    socket: String,
    address: String,
}

impl HttpMock {
    /// Returns the socket for configuring the Httpmock service.
    pub fn socket(&self) -> &str {
        &self.socket
    }

    /// Returns the address of the Httpmock service.
    pub fn address(&self) -> &str {
        &self.address
    }
}

impl Service for HttpMock {
    type Config = HttpMockConfig;

    fn new(config: &Self::Config, container: &Container) -> Self {
        let socket = container
            .socket(config.port)
            .expect("port should be configured")
            .to_string();
        let address = format!("{SCHEMA}://{socket}");
        Self { socket, address }
    }
}