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

//! gRPC testing service for integration testing
//!
//! This module provides predefined types and configurations for GrpcBin services
//! in integration tests. [GrpcBin](https://github.com/moul/grpcbin) is a gRPC
//! testing service useful for testing gRPC clients and protocol interactions.
//!

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 INSECURE_PORT: Port = 9000;
const SECURE_PORT: Port = 9001;
const GRPC_SCHEMA: &str = "grpc";

/// Default GrpcBin Docker image name.
pub const GRPCBIN_IMAGE_NAME: &str = "moul/grpcbin";

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

impl GrpcBinConfig {
    /// Returns the `GrpcBin` service hostname.
    pub fn hostname(&self) -> &str {
        &self.hostname
    }

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

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

    /// Returns the `GrpcBin` [`PortVisibility`].
    pub fn visibility(&self) -> PortVisibility {
        self.visibility
    }

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

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

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

/// Builder for [`GrpcBinConfig`].
pub struct GrpcBinConfigBuilder {
    config: GrpcBinConfig,
}

impl GrpcBinConfigBuilder {
    fn new() -> Self {
        Self {
            config: GrpcBinConfig::new(),
        }
    }

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

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

    /// Sets the `GrpcBin` [`PortVisibility`].
    /// By default set to [`PortVisibility::Exposed`].
    pub fn visibility(self, visibility: PortVisibility) -> Self {
        Self {
            config: GrpcBinConfig {
                visibility,
                ..self.config
            },
        }
    }

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

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

    /// Builds a [`GrpcBinConfig`] with the configured values.
    pub fn build(self) -> GrpcBinConfig {
        self.config
    }
}

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

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

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

    fn schema(&self) -> &str {
        GRPC_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(INSECURE_PORT, self.visibility),
            PortAccess::new(SECURE_PORT, self.visibility),
        ])
        .readiness(
            MessageProbe::builder("listening on :")
                .times(2)
                .timeout(Duration::from_secs(30))
                .source(MessageSource::StdErr)
                .build(),
        )
        .build())
    }
}

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

impl GrpcBin {
    /// Returns the `GrpcBin` external socket.
    pub fn socket(&self) -> Option<&str> {
        self.socket.as_deref()
    }

    /// Returns the `GrpcBin` external address.
    pub fn address(&self) -> Option<&str> {
        self.address.as_deref()
    }
}

impl Service for GrpcBin {
    type Config = GrpcBinConfig;

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

        Self { socket, address }
    }
}