use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
use mockall::{
    mock,
    predicate::{self, always, eq},
    Predicate,
};

#[derive(Clone, Debug)]
pub enum RecorderOperation {
    DescribeCounter(KeyName, Option<Unit>, SharedString),
    DescribeGauge(KeyName, Option<Unit>, SharedString),
    DescribeHistogram(KeyName, Option<Unit>, SharedString),
    RegisterCounter(Key, Counter, &'static Metadata<'static>),
    RegisterGauge(Key, Gauge, &'static Metadata<'static>),
    RegisterHistogram(Key, Histogram, &'static Metadata<'static>),
}

impl RecorderOperation {
    fn apply_to_mock(self, mock: &mut MockBasicRecorder) {
        match self {
            RecorderOperation::DescribeCounter(key_name, unit, desc) => {
                expect_describe_counter(mock, key_name, unit, desc)
            }
            RecorderOperation::DescribeGauge(key_name, unit, desc) => {
                expect_describe_gauge(mock, key_name, unit, desc)
            }
            RecorderOperation::DescribeHistogram(key_name, unit, desc) => {
                expect_describe_histogram(mock, key_name, unit, desc)
            }
            RecorderOperation::RegisterCounter(key, counter, _) => {
                expect_register_counter(mock, key, counter)
            }
            RecorderOperation::RegisterGauge(key, gauge, _) => {
                expect_register_gauge(mock, key, gauge)
            }
            RecorderOperation::RegisterHistogram(key, histogram, _) => {
                expect_register_histogram(mock, key, histogram)
            }
        }
    }

    pub fn apply_to_recorder<R>(self, recorder: &R)
    where
        R: Recorder,
    {
        match self {
            RecorderOperation::DescribeCounter(key_name, unit, desc) => {
                recorder.describe_counter(key_name, unit, desc);
            }
            RecorderOperation::DescribeGauge(key_name, unit, desc) => {
                recorder.describe_gauge(key_name, unit, desc);
            }
            RecorderOperation::DescribeHistogram(key_name, unit, desc) => {
                recorder.describe_histogram(key_name, unit, desc);
            }
            RecorderOperation::RegisterCounter(key, _, metadata) => {
                let _ = recorder.register_counter(&key, metadata);
            }
            RecorderOperation::RegisterGauge(key, _, metadata) => {
                let _ = recorder.register_gauge(&key, metadata);
            }
            RecorderOperation::RegisterHistogram(key, _, metadata) => {
                let _ = recorder.register_histogram(&key, metadata);
            }
        }
    }
}

mock! {
    #[derive(Debug)]
    pub BasicRecorder {}

    impl Recorder for BasicRecorder {
        fn describe_counter(&self, key_name: KeyName, unit: Option<Unit>, description: SharedString);
        fn describe_gauge(&self, key_name: KeyName, unit: Option<Unit>, description: SharedString);
        fn describe_histogram(&self, key_name: KeyName, unit: Option<Unit>, description: SharedString);
        fn register_counter<'a>(&'a self, key: &'a Key, metadata: &'a Metadata<'a>) -> Counter;
        fn register_gauge<'a>(&'a self, key: &'a Key, metadata: &'a Metadata<'a>) -> Gauge;
        fn register_histogram<'a>(&'a self, key: &'a Key, metadata: &'a Metadata<'a>) -> Histogram;
    }
}

impl MockBasicRecorder {
    pub fn from_operations<O>(operations: O) -> Self
    where
        O: IntoIterator<Item = RecorderOperation>,
    {
        let mut recorder = Self::new();
        for operation in operations.into_iter() {
            operation.apply_to_mock(&mut recorder);
        }
        recorder
    }
}

pub fn expect_describe_counter(
    mock: &mut MockBasicRecorder,
    key_name: KeyName,
    unit: Option<Unit>,
    description: SharedString,
) {
    mock.expect_describe_counter()
        .times(1)
        .with(eq(key_name), eq(unit), eq(description))
        .return_const(());
}

pub fn expect_describe_gauge(
    mock: &mut MockBasicRecorder,
    key_name: KeyName,
    unit: Option<Unit>,
    description: SharedString,
) {
    mock.expect_describe_gauge()
        .times(1)
        .with(eq(key_name), eq(unit), eq(description))
        .return_const(());
}

pub fn expect_describe_histogram(
    mock: &mut MockBasicRecorder,
    key_name: KeyName,
    unit: Option<Unit>,
    description: SharedString,
) {
    mock.expect_describe_histogram()
        .times(1)
        .with(eq(key_name), eq(unit), eq(description))
        .return_const(());
}

pub fn expect_register_counter(mock: &mut MockBasicRecorder, key: Key, counter: Counter) {
    mock.expect_register_counter().times(1).with(ref_eq(key), always()).return_const(counter);
}

pub fn expect_register_gauge(mock: &mut MockBasicRecorder, key: Key, gauge: Gauge) {
    mock.expect_register_gauge().times(1).with(ref_eq(key), always()).return_const(gauge);
}

pub fn expect_register_histogram(mock: &mut MockBasicRecorder, key: Key, histogram: Histogram) {
    mock.expect_register_histogram().times(1).with(ref_eq(key), always()).return_const(histogram);
}

fn ref_eq<T: PartialEq>(value: T) -> impl Predicate<T> {
    predicate::function(move |item: &T| item == &value)
}
