use crate::harness::attributes_to_map;
use crate::harness::build_metrics_with_defaults;
use crate::harness::find_metric;
use crate::harness::histogram_data;
use crate::harness::latest_metrics;
use codex_otel::Result;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;

// Ensures counters/histograms render with default + per-call tags.
#[test]
fn send_builds_payload_with_tags_and_histograms() -> Result<()> {
    let (metrics, exporter) =
        build_metrics_with_defaults(&[("service", "codex-cli"), ("env", "prod")])?;

    metrics.counter_with_description(
        "codex.turns",
        "Total number of Codex turns.",
        /*inc*/ 1,
        &[("model", "gpt-5.1"), ("env", "dev")],
    )?;
    metrics.histogram(
        "codex.tool_latency",
        /*value*/ 25,
        &[("tool", "shell")],
    )?;
    metrics.gauge_with_description(
        "codex.active",
        "Number of active Codex operations.",
        /*value*/ 2,
        &[("component", "test")],
    )?;
    metrics.shutdown()?;

    let resource_metrics = latest_metrics(&exporter);

    let counter = find_metric(&resource_metrics, "codex.turns").expect("counter metric missing");
    assert_eq!(counter.description(), "Total number of Codex turns.");
    let counter_attributes = match counter.data() {
        opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
            opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
                let points: Vec<_> = sum.data_points().collect();
                assert_eq!(points.len(), 1);
                assert_eq!(points[0].value(), 1);
                attributes_to_map(points[0].attributes())
            }
            _ => panic!("unexpected counter aggregation"),
        },
        _ => panic!("unexpected counter data type"),
    };

    let expected_counter_attributes = BTreeMap::from([
        ("service".to_string(), "codex-cli".to_string()),
        ("env".to_string(), "dev".to_string()),
        ("model".to_string(), "gpt-5.1".to_string()),
    ]);
    assert_eq!(counter_attributes, expected_counter_attributes);

    let (bounds, bucket_counts, sum, count) =
        histogram_data(&resource_metrics, "codex.tool_latency");
    assert!(!bounds.is_empty());
    assert_eq!(bucket_counts.iter().sum::<u64>(), 1);
    assert_eq!(sum, 25.0);
    assert_eq!(count, 1);

    let histogram_attrs = attributes_to_map(
        match find_metric(&resource_metrics, "codex.tool_latency").and_then(|metric| {
            match metric.data() {
                opentelemetry_sdk::metrics::data::AggregatedMetrics::F64(
                    opentelemetry_sdk::metrics::data::MetricData::Histogram(histogram),
                ) => histogram
                    .data_points()
                    .next()
                    .map(opentelemetry_sdk::metrics::data::HistogramDataPoint::attributes),
                _ => None,
            }
        }) {
            Some(attrs) => attrs,
            None => panic!("histogram attributes missing"),
        },
    );
    let expected_histogram_attributes = BTreeMap::from([
        ("service".to_string(), "codex-cli".to_string()),
        ("env".to_string(), "prod".to_string()),
        ("tool".to_string(), "shell".to_string()),
    ]);
    assert_eq!(histogram_attrs, expected_histogram_attributes);

    let gauge = find_metric(&resource_metrics, "codex.active").expect("gauge metric missing");
    assert_eq!(gauge.description(), "Number of active Codex operations.");
    let gauge_point = match gauge.data() {
        opentelemetry_sdk::metrics::data::AggregatedMetrics::I64(data) => match data {
            opentelemetry_sdk::metrics::data::MetricData::Gauge(gauge) => {
                gauge.data_points().next().expect("gauge point")
            }
            _ => panic!("unexpected gauge aggregation"),
        },
        _ => panic!("unexpected gauge metric data type"),
    };
    assert_eq!(gauge_point.value(), 2);
    assert_eq!(
        attributes_to_map(gauge_point.attributes()),
        BTreeMap::from([
            ("component".to_string(), "test".to_string()),
            ("env".to_string(), "prod".to_string()),
            ("service".to_string(), "codex-cli".to_string()),
        ])
    );

    Ok(())
}

// Ensures defaults merge per line and overrides take precedence.
#[test]
fn send_merges_default_tags_per_line() -> Result<()> {
    let (metrics, exporter) = build_metrics_with_defaults(&[
        ("service", "codex-cli"),
        ("env", "prod"),
        ("region", "us"),
    ])?;

    metrics.counter(
        "codex.alpha",
        /*inc*/ 1,
        &[("env", "dev"), ("component", "alpha")],
    )?;
    metrics.counter(
        "codex.beta",
        /*inc*/ 2,
        &[("service", "worker"), ("component", "beta")],
    )?;
    metrics.shutdown()?;

    let resource_metrics = latest_metrics(&exporter);
    let alpha_metric =
        find_metric(&resource_metrics, "codex.alpha").expect("codex.alpha metric missing");
    let alpha_point = match alpha_metric.data() {
        opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
            opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
                let points: Vec<_> = sum.data_points().collect();
                assert_eq!(points.len(), 1);
                points[0]
            }
            _ => panic!("unexpected counter aggregation"),
        },
        _ => panic!("unexpected counter data type"),
    };
    assert_eq!(alpha_point.value(), 1);
    let alpha_attrs = attributes_to_map(alpha_point.attributes());
    let expected_alpha_attrs = BTreeMap::from([
        ("component".to_string(), "alpha".to_string()),
        ("env".to_string(), "dev".to_string()),
        ("region".to_string(), "us".to_string()),
        ("service".to_string(), "codex-cli".to_string()),
    ]);
    assert_eq!(alpha_attrs, expected_alpha_attrs);

    let beta_metric =
        find_metric(&resource_metrics, "codex.beta").expect("codex.beta metric missing");
    let beta_point = match beta_metric.data() {
        opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
            opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
                let points: Vec<_> = sum.data_points().collect();
                assert_eq!(points.len(), 1);
                points[0]
            }
            _ => panic!("unexpected counter aggregation"),
        },
        _ => panic!("unexpected counter data type"),
    };
    assert_eq!(beta_point.value(), 2);
    let beta_attrs = attributes_to_map(beta_point.attributes());
    let expected_beta_attrs = BTreeMap::from([
        ("component".to_string(), "beta".to_string()),
        ("env".to_string(), "prod".to_string()),
        ("region".to_string(), "us".to_string()),
        ("service".to_string(), "worker".to_string()),
    ]);
    assert_eq!(beta_attrs, expected_beta_attrs);

    Ok(())
}

// Verifies enqueued metrics are delivered by the background worker.
#[test]
fn client_sends_enqueued_metric() -> Result<()> {
    let (metrics, exporter) = build_metrics_with_defaults(&[])?;

    metrics.counter("codex.turns", /*inc*/ 1, &[("model", "gpt-5.1")])?;
    metrics.shutdown()?;

    let resource_metrics = latest_metrics(&exporter);
    let counter = find_metric(&resource_metrics, "codex.turns").expect("counter metric missing");
    let points = match counter.data() {
        opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
            opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
                sum.data_points().collect::<Vec<_>>()
            }
            _ => panic!("unexpected counter aggregation"),
        },
        _ => panic!("unexpected counter data type"),
    };
    assert_eq!(points.len(), 1);
    let point = points[0];
    assert_eq!(point.value(), 1);
    let attrs = attributes_to_map(point.attributes());
    assert_eq!(attrs.get("model").map(String::as_str), Some("gpt-5.1"));

    Ok(())
}

// Ensures shutdown flushes successfully with in-memory exporters.
#[test]
fn shutdown_flushes_in_memory_exporter() -> Result<()> {
    let (metrics, exporter) = build_metrics_with_defaults(&[])?;

    metrics.counter("codex.turns", /*inc*/ 1, &[])?;
    metrics.shutdown()?;

    let resource_metrics = latest_metrics(&exporter);
    let counter = find_metric(&resource_metrics, "codex.turns").expect("counter metric missing");
    let points = match counter.data() {
        opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
            opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
                sum.data_points().collect::<Vec<_>>()
            }
            _ => panic!("unexpected counter aggregation"),
        },
        _ => panic!("unexpected counter data type"),
    };
    assert_eq!(points.len(), 1);

    Ok(())
}

// Ensures shutting down without recording metrics does not export anything.
#[test]
fn shutdown_without_metrics_exports_nothing() -> Result<()> {
    let (metrics, exporter) = build_metrics_with_defaults(&[])?;

    metrics.shutdown()?;

    let finished = exporter.get_finished_metrics().unwrap();
    assert!(finished.is_empty(), "expected no metrics exported");
    Ok(())
}
