# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

# pylint: disable=protected-access,no-self-use,too-many-lines
import json
import os
import subprocess
import sys
import unittest
import weakref
from collections.abc import Callable, Iterable, Sequence
from logging import DEBUG, WARNING
from pathlib import Path
from threading import Lock
from time import sleep
from typing import Any
from unittest.mock import MagicMock, Mock, patch

from opentelemetry.attributes import BoundedAttributes
from opentelemetry.metrics import NoOpMeter
from opentelemetry.sdk.environment_variables import (
    OTEL_EXPERIMENTAL_RESOURCE_DETECTORS,
    OTEL_SDK_DISABLED,
)
from opentelemetry.sdk.metrics import (
    Counter,
    Histogram,
    Meter,
    MeterProvider,
    ObservableCounter,
    ObservableGauge,
    ObservableUpDownCounter,
    UpDownCounter,
    _Gauge,
)
from opentelemetry.sdk.metrics._internal import (
    SynchronousMeasurementConsumer,
    _default_meter_configurator,
    _disable_meter_configurator,
    _MeterConfig,
    _ProxyMeterConfig,
    _RuleBasedMeterConfigurator,
)
from opentelemetry.sdk.metrics.export import (
    InMemoryMetricReader,
    Metric,
    MetricExporter,
    MetricExportResult,
    MetricReader,
    PeriodicExportingMetricReader,
)
from opentelemetry.sdk.metrics.view import SumAggregation, View
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.util.instrumentation import (
    InstrumentationScope,
    _scope_name_matches_glob,
)
from opentelemetry.test import TestCase
from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc


class DummyMetricReader(MetricReader):
    def __init__(self):
        super().__init__()

    def _receive_metrics(
        self,
        metrics_data: Iterable[Metric],
        timeout_millis: float = 10_000,
        **kwargs,
    ) -> None:
        pass

    def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
        return True


# pylint: disable=too-many-public-methods
class TestMeterProvider(ConcurrencyTestBase, TestCase):
    def tearDown(self):
        MeterProvider._all_metric_readers = weakref.WeakSet()

    @patch.object(Resource, "create")
    def test_init_default(self, resource_patch):
        meter_provider = MeterProvider()
        resource_mock = resource_patch.return_value
        resource_patch.assert_called_once()
        self.assertIsNotNone(meter_provider._sdk_config)
        self.assertEqual(meter_provider._sdk_config.resource, resource_mock)
        self.assertTrue(
            isinstance(
                meter_provider._measurement_consumer,
                SynchronousMeasurementConsumer,
            )
        )
        self.assertIsNotNone(meter_provider._atexit_handler)

    def test_register_metric_readers(self):
        mock_exporter = Mock()
        mock_exporter._preferred_temporality = None
        mock_exporter._preferred_aggregation = None
        metric_reader_0 = PeriodicExportingMetricReader(mock_exporter)
        metric_reader_1 = PeriodicExportingMetricReader(mock_exporter)

        with self.assertNotRaises(Exception):
            MeterProvider(metric_readers=(metric_reader_0,))
            MeterProvider(metric_readers=(metric_reader_1,))

        with self.assertRaises(Exception):
            MeterProvider(metric_readers=(metric_reader_0,))
            MeterProvider(metric_readers=(metric_reader_0,))

    def test_resource(self):
        """
        `MeterProvider` provides a way to allow a `Resource` to be specified.
        """

        meter_provider_0 = MeterProvider()
        meter_provider_1 = MeterProvider()

        self.assertEqual(
            meter_provider_0._sdk_config.resource,
            meter_provider_1._sdk_config.resource,
        )
        self.assertIsInstance(meter_provider_0._sdk_config.resource, Resource)
        self.assertIsInstance(meter_provider_1._sdk_config.resource, Resource)

        resource = Resource({"key": "value"})
        self.assertIs(
            MeterProvider(resource=resource)._sdk_config.resource, resource
        )

    def test_update_resource(self):
        initial_resource = Resource({"one": "one", "two": "old"})
        updating_resource = Resource({"two": "new", "three": "three"})
        reader = InMemoryMetricReader()
        meter_provider = MeterProvider(
            metric_readers=[reader], resource=initial_resource
        )
        meter = meter_provider.get_meter("name")
        counter = meter.create_counter("counter")

        meter_provider._update_resource(updating_resource)

        self.assertEqual(
            meter_provider._sdk_config.resource.attributes,
            {"one": "one", "two": "new", "three": "three"},
        )

        counter.add(1)
        metrics_data = reader.get_metrics_data()
        self.assertIs(
            metrics_data.resource_metrics[0].resource,
            meter_provider._sdk_config.resource,
        )

        new_meter = meter_provider.get_meter("new")
        new_counter = new_meter.create_counter("new_counter")
        new_counter.add(1)
        metrics_data = reader.get_metrics_data()
        self.assertIs(
            metrics_data.resource_metrics[0].resource,
            meter_provider._sdk_config.resource,
        )

    @unittest.skipUnless(
        hasattr(os, "fork") and hasattr(os, "register_at_fork"),
        "requires os.fork and os.register_at_fork",
    )
    def test_meter_provider_updates_process_dependent_resource_after_fork(
        self,
    ):
        script_path = (
            Path(__file__).parent
            / "scripts"
            / "meter_provider_resource_after_fork.py"
        )

        result = subprocess.run(
            [sys.executable, str(script_path)],
            capture_output=True,
            check=False,
            text=True,
            env={
                **os.environ,
                OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "process",
            },
        )
        self.assertEqual(
            result.returncode,
            0,
            f"stdout: {result.stdout}\nstderr: {result.stderr}",
        )
        lines = result.stdout.strip().splitlines()
        child_payload = json.loads(lines[0])
        parent_payload = json.loads(lines[1])

        self.assertEqual(
            parent_payload["parent_resource_pid"], parent_payload["parent_pid"]
        )
        self.assertEqual(
            parent_payload["parent_resource_pid_after_fork"],
            parent_payload["parent_pid"],
        )

        self.assertNotEqual(
            child_payload["child_pid"], parent_payload["parent_pid"]
        )
        self.assertEqual(
            child_payload["provider_pid"], child_payload["child_pid"]
        )
        self.assertEqual(
            child_payload["exported_resource_pids"],
            [child_payload["child_pid"]],
        )
        self.assertEqual(
            child_payload["metric_names"],
            ["cached_counter", "new_counter"],
        )

    def test_get_meter(self):
        """
        `MeterProvider.get_meter` arguments are used to create an
        `InstrumentationScope` object on the created `Meter`.
        """

        meter = MeterProvider().get_meter(
            "name",
            version="version",
            schema_url="schema_url",
            attributes={"key": "value"},
        )

        self.assertEqual(meter._instrumentation_scope.name, "name")
        self.assertEqual(meter._instrumentation_scope.version, "version")
        self.assertEqual(meter._instrumentation_scope.schema_url, "schema_url")
        self.assertEqual(
            meter._instrumentation_scope.attributes, {"key": "value"}
        )

    def test_get_meter_attributes(self):
        """
        `MeterProvider.get_meter` arguments are used to create an
        `InstrumentationScope` object on the created `Meter`.
        """

        meter = MeterProvider().get_meter(
            "name",
            version="version",
            schema_url="schema_url",
            attributes={"key": "value", "key2": 5, "key3": "value3"},
        )

        self.assertEqual(meter._instrumentation_scope.name, "name")
        self.assertEqual(meter._instrumentation_scope.version, "version")
        self.assertEqual(meter._instrumentation_scope.schema_url, "schema_url")
        self.assertEqual(
            meter._instrumentation_scope.attributes,
            {"key": "value", "key2": 5, "key3": "value3"},
        )

    def test_get_meter_empty(self):
        """
        `MeterProvider.get_meter` called with None or empty string as name
        should return a NoOpMeter.
        """

        with self.assertLogs(level=WARNING):
            meter = MeterProvider().get_meter(
                None,
                version="version",
                schema_url="schema_url",
            )
        self.assertIsInstance(meter, NoOpMeter)
        self.assertEqual(meter._name, None)

        with self.assertLogs(level=WARNING):
            meter = MeterProvider().get_meter(
                "",
                version="version",
                schema_url="schema_url",
            )
        self.assertIsInstance(meter, NoOpMeter)
        self.assertEqual(meter._name, "")

    def test_get_meter_duplicate(self):
        """
        Subsequent calls to `MeterProvider.get_meter` with the same arguments
        should return the same `Meter` instance.
        """
        mp = MeterProvider()
        meter1 = mp.get_meter(
            "name",
            version="version",
            schema_url="schema_url",
        )
        meter2 = mp.get_meter(
            "name",
            version="version",
            schema_url="schema_url",
        )
        meter3 = mp.get_meter(
            "name2",
            version="version",
            schema_url="schema_url",
        )
        self.assertIs(meter1, meter2)
        self.assertIsNot(meter1, meter3)

    def test_get_meter_comparison_with_attributes(self):
        """
        Subsequent calls to `MeterProvider.get_meter` with the same arguments
        should return the same `Meter` instance.
        """
        mp = MeterProvider()
        meter1 = mp.get_meter(
            "name",
            version="version",
            schema_url="schema_url",
            attributes={"key": "value", "key2": 5, "key3": "value3"},
        )
        meter2 = mp.get_meter(
            "name",
            version="version",
            schema_url="schema_url",
            attributes={"key": "value", "key2": 5, "key3": "value3"},
        )
        meter3 = mp.get_meter(
            "name2",
            version="version",
            schema_url="schema_url",
        )
        meter4 = mp.get_meter(
            "name",
            version="version",
            schema_url="schema_url",
            attributes={"key": "value", "key2": 5, "key3": "value4"},
        )
        self.assertIs(meter1, meter2)
        self.assertIsNot(meter1, meter3)
        self.assertTrue(
            meter3._instrumentation_scope > meter4._instrumentation_scope
        )
        self.assertIsInstance(
            meter4._instrumentation_scope.attributes, BoundedAttributes
        )

    def test_shutdown(self):
        mock_metric_reader_0 = MagicMock(
            **{
                "shutdown.side_effect": ZeroDivisionError(),
            }
        )
        mock_metric_reader_1 = MagicMock(
            **{
                "shutdown.side_effect": AssertionError(),
            }
        )

        meter_provider = MeterProvider(
            metric_readers=[mock_metric_reader_0, mock_metric_reader_1]
        )

        with self.assertRaises(Exception) as error:
            meter_provider.shutdown()

        error = error.exception

        self.assertEqual(
            str(error),
            (
                "MeterProvider.shutdown failed because the following "
                "metric readers failed during shutdown:\n"
                "MagicMock: ZeroDivisionError()\n"
                "MagicMock: AssertionError()"
            ),
        )

        mock_metric_reader_0.shutdown.assert_called_once()
        mock_metric_reader_1.shutdown.assert_called_once()

        mock_metric_reader_0 = Mock()
        mock_metric_reader_1 = Mock()

        meter_provider = MeterProvider(
            metric_readers=[mock_metric_reader_0, mock_metric_reader_1]
        )

        self.assertIsNone(meter_provider.shutdown())
        mock_metric_reader_0.shutdown.assert_called_once()
        mock_metric_reader_1.shutdown.assert_called_once()

    def test_shutdown_subsequent_calls(self):
        """
        No subsequent attempts to get a `Meter` are allowed after calling
        `MeterProvider.shutdown`
        """

        meter_provider = MeterProvider()

        with self.assertRaises(AssertionError):
            with self.assertLogs(level=WARNING):
                meter_provider.shutdown()

        with self.assertLogs(level=WARNING):
            meter_provider.shutdown()

    @patch("opentelemetry.sdk.metrics._internal._logger")
    def test_shutdown_race(self, mock_logger):
        mock_logger.warning = MockFunc()
        meter_provider = MeterProvider()
        num_threads = 70
        self.run_with_many_threads(
            meter_provider.shutdown, num_threads=num_threads
        )
        self.assertEqual(mock_logger.warning.call_count, num_threads - 1)

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_measurement_collect_callback(
        self, mock_sync_measurement_consumer
    ):
        metric_readers = [
            DummyMetricReader(),
            DummyMetricReader(),
            DummyMetricReader(),
            DummyMetricReader(),
            DummyMetricReader(),
        ]
        sync_consumer_instance = mock_sync_measurement_consumer()
        sync_consumer_instance.collect = MockFunc()
        MeterProvider(metric_readers=metric_readers)

        for reader in metric_readers:
            reader.collect()
        self.assertEqual(
            sync_consumer_instance.collect.call_count, len(metric_readers)
        )

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_creates_sync_measurement_consumer(
        self, mock_sync_measurement_consumer
    ):
        MeterProvider()
        mock_sync_measurement_consumer.assert_called()

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_register_asynchronous_instrument(
        self, mock_sync_measurement_consumer
    ):
        meter_provider = MeterProvider()

        # pylint: disable=no-member
        meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with(
            meter_provider.get_meter("name").create_observable_counter(
                "name0", callbacks=[Mock()]
            )
        )
        meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with(
            meter_provider.get_meter("name").create_observable_up_down_counter(
                "name1", callbacks=[Mock()]
            )
        )
        meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with(
            meter_provider.get_meter("name").create_observable_gauge(
                "name2", callbacks=[Mock()]
            )
        )

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_consume_measurement_counter(self, mock_sync_measurement_consumer):
        sync_consumer_instance = mock_sync_measurement_consumer()
        meter_provider = MeterProvider()
        counter = meter_provider.get_meter("name").create_counter("name")

        counter.add(1)

        sync_consumer_instance.consume_measurement.assert_called()

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_consume_measurement_up_down_counter(
        self, mock_sync_measurement_consumer
    ):
        sync_consumer_instance = mock_sync_measurement_consumer()
        meter_provider = MeterProvider()
        counter = meter_provider.get_meter("name").create_up_down_counter(
            "name"
        )

        counter.add(1)

        sync_consumer_instance.consume_measurement.assert_called()

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_consume_measurement_histogram(
        self, mock_sync_measurement_consumer
    ):
        sync_consumer_instance = mock_sync_measurement_consumer()
        meter_provider = MeterProvider()
        counter = meter_provider.get_meter("name").create_histogram("name")

        counter.record(1)

        sync_consumer_instance.consume_measurement.assert_called()

    def test_meter_provider_with_disabled_configurator(self):
        mp = MeterProvider(_meter_configurator=_disable_meter_configurator)
        meter = mp.get_meter("test")
        self.assertFalse(meter._is_enabled())

    def test_meter_provider_with_custom_configurator(self):
        def configurator(scope):
            if scope.name == "disabled_meter":
                return _MeterConfig(is_enabled=False)
            return _MeterConfig.default()

        mp = MeterProvider(_meter_configurator=configurator)
        enabled = mp.get_meter("enabled_meter")
        disabled = mp.get_meter("disabled_meter")
        self.assertTrue(enabled._is_enabled())
        self.assertFalse(disabled._is_enabled())

    def test_set_meter_configurator_updates_existing_meters(self):
        mp = MeterProvider()
        meter = mp.get_meter("test")
        self.assertTrue(meter._is_enabled())

        mp._set_meter_configurator(
            meter_configurator=_disable_meter_configurator
        )
        self.assertFalse(meter._is_enabled())

    def test_set_meter_configurator_affects_new_meters(self):
        mp = MeterProvider()
        mp._set_meter_configurator(
            meter_configurator=_disable_meter_configurator
        )
        meter = mp.get_meter("new_meter")
        self.assertFalse(meter._is_enabled())

    def test_buggy_configurator_falls_back_to_default_on_get_meter(self):
        def raising_configurator(_scope):
            raise RuntimeError("configurator error")

        mp = MeterProvider(_meter_configurator=raising_configurator)
        with self.assertLogs(level="ERROR"):
            meter = mp.get_meter("test")
        self.assertTrue(meter._is_enabled())

    def test_buggy_configurator_falls_back_to_default_on_set_configurator(
        self,
    ):
        mp = MeterProvider()
        meter = mp.get_meter("test")
        self.assertTrue(meter._is_enabled())

        def raising_configurator(_scope):
            raise ValueError("bad config")

        with self.assertLogs(level="ERROR"):
            mp._set_meter_configurator(meter_configurator=raising_configurator)
        # Should still be enabled (default config) despite the error
        self.assertTrue(meter._is_enabled())

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_consume_measurement_gauge(self, mock_sync_measurement_consumer):
        sync_consumer_instance = mock_sync_measurement_consumer()
        meter_provider = MeterProvider()
        gauge = meter_provider.get_meter("name").create_gauge("name")

        gauge.set(1)

        sync_consumer_instance.consume_measurement.assert_called()

    def test_addition_of_metric_reader(self):
        internal_logger = "opentelemetry.sdk.metrics._internal"
        export_logger = "opentelemetry.sdk.metrics._internal.export"

        reader = InMemoryMetricReader()
        meter_provider = MeterProvider()
        meter = meter_provider.get_meter(__name__)
        counter = meter.create_counter("counter")
        counter.add(1)
        # Suppress warnings for calling collect on an unregistered metric reader
        with self.assertLogs(export_logger, DEBUG):
            self.assertIsNone(reader.get_metrics_data())

        meter_provider.add_metric_reader(reader)
        counter.add(1)
        self.assertIsNotNone(reader.get_metrics_data())

        with self.assertLogs(internal_logger, DEBUG) as cm:
            meter_provider.add_metric_reader(reader)
            self.assertIn("has been registered already!", cm.output[0])

        meter_provider.remove_metric_reader(reader)
        counter.add(1)
        with self.assertLogs(export_logger, DEBUG):
            self.assertIsNone(reader.get_metrics_data())

        with self.assertLogs(internal_logger, DEBUG) as cm:
            meter_provider.remove_metric_reader(reader)
            self.assertIn("has not been registered!", cm.output[0])


class TestMeterConcurrency(ConcurrencyTestBase, TestCase):
    def test_create_instrument_concurrency(self):
        """
        Tests that concurrent creation of the same instrument does not
        result in a KeyError or inconsistent state for all instrument types.
        """

        meter = Meter(Mock(), Mock())
        original_register = meter._register_instrument
        lock = Lock()
        registered_names = set()

        def mocked_register(name: str, *args, **kwargs):
            status = original_register(name, *args, **kwargs)
            with lock:
                first: bool = name not in registered_names
                registered_names.add(name)

            if first:
                # Test interleaving of threads by sleeping after the first thread registers
                # the instrument, but before the instrument is created.
                sleep(0.25)
            return status

        def make_create_instrument(
            meter: Meter, method_name: str, args: list[Any]
        ) -> Callable[[], Any]:
            return lambda: getattr(meter, method_name)(
                f"concurrent_{method_name}", *args
            )

        with patch.object(
            meter, "_register_instrument", side_effect=mocked_register
        ):
            create_methods = [
                ("create_counter", []),
                ("create_up_down_counter", []),
                ("create_histogram", []),
                ("create_gauge", []),
                ("create_observable_counter", [[lambda options: []]]),
                ("create_observable_gauge", [[lambda options: []]]),
                ("create_observable_up_down_counter", [[lambda options: []]]),
            ]

            for method_name, args in create_methods:
                with self.subTest(method=method_name):
                    instruments = self.run_with_many_threads(
                        make_create_instrument(meter, method_name, args),
                        num_threads=20,
                    )

                    for instr in instruments:
                        self.assertIs(instr, instruments[0])


class TestMeter(TestCase):
    def setUp(self):
        self.meter = Meter(Mock(), Mock())

    def test_repeated_instrument_names(self):
        with self.assertNotRaises(Exception):
            self.meter.create_counter("counter")
            self.meter.create_up_down_counter("up_down_counter")
            self.meter.create_observable_counter(
                "observable_counter", callbacks=[Mock()]
            )
            self.meter.create_histogram("histogram")
            self.meter.create_gauge("gauge")
            self.meter.create_observable_gauge(
                "observable_gauge", callbacks=[Mock()]
            )
            self.meter.create_observable_up_down_counter(
                "observable_up_down_counter", callbacks=[Mock()]
            )

        for instrument_name in [
            "counter",
            "up_down_counter",
            "histogram",
            "gauge",
        ]:
            with self.assertNoLogs(
                "opentelemetry.sdk.metrics._internal", level="WARNING"
            ):
                getattr(self.meter, f"create_{instrument_name}")(
                    instrument_name
                )

        for instrument_name in [
            "observable_counter",
            "observable_gauge",
            "observable_up_down_counter",
        ]:
            with self.assertNoLogs(
                "opentelemetry.sdk.metrics._internal", level="WARNING"
            ):
                getattr(self.meter, f"create_{instrument_name}")(
                    instrument_name, callbacks=[Mock()]
                )

    def test_repeated_instrument_names_with_different_advisory(self):
        with self.assertNotRaises(Exception):
            self.meter.create_histogram(
                "histogram", explicit_bucket_boundaries_advisory=[1.0]
            )

        for instrument_name in [
            "histogram",
        ]:
            with self.assertLogs(level=WARNING):
                getattr(self.meter, f"create_{instrument_name}")(
                    instrument_name
                )

    def test_create_counter(self):
        counter = self.meter.create_counter(
            "name", unit="unit", description="description"
        )

        self.assertIsInstance(counter, Counter)
        self.assertEqual(counter.name, "name")

    def test_create_up_down_counter(self):
        up_down_counter = self.meter.create_up_down_counter(
            "name", unit="unit", description="description"
        )

        self.assertIsInstance(up_down_counter, UpDownCounter)
        self.assertEqual(up_down_counter.name, "name")

    def test_create_observable_counter(self):
        observable_counter = self.meter.create_observable_counter(
            "name", callbacks=[Mock()], unit="unit", description="description"
        )

        self.assertIsInstance(observable_counter, ObservableCounter)
        self.assertEqual(observable_counter.name, "name")

    def test_create_histogram(self):
        histogram = self.meter.create_histogram(
            "name", unit="unit", description="description"
        )

        self.assertIsInstance(histogram, Histogram)
        self.assertEqual(histogram.name, "name")

    def test_create_histogram_with_advisory(self):
        histogram = self.meter.create_histogram(
            "name",
            unit="unit",
            description="description",
            explicit_bucket_boundaries_advisory=[0.0, 1.0, 2],
        )

        self.assertIsInstance(histogram, Histogram)
        self.assertEqual(histogram.name, "name")
        self.assertEqual(
            histogram._advisory.explicit_bucket_boundaries,
            [0.0, 1.0, 2],
        )

    def test_create_histogram_advisory_validation(self):
        advisories = [
            {"explicit_bucket_boundaries_advisory": "hello"},
            {"explicit_bucket_boundaries_advisory": ["1"]},
        ]
        for advisory in advisories:
            with self.subTest(advisory=advisory):
                with self.assertLogs(level=WARNING):
                    self.meter.create_histogram(
                        "name",
                        unit="unit",
                        description="description",
                        **advisory,
                    )

    def test_create_observable_gauge(self):
        observable_gauge = self.meter.create_observable_gauge(
            "name", callbacks=[Mock()], unit="unit", description="description"
        )

        self.assertIsInstance(observable_gauge, ObservableGauge)
        self.assertEqual(observable_gauge.name, "name")

    def test_create_gauge(self):
        gauge = self.meter.create_gauge(
            "name", unit="unit", description="description"
        )

        self.assertIsInstance(gauge, _Gauge)
        self.assertEqual(gauge.name, "name")

    def test_create_observable_up_down_counter(self):
        observable_up_down_counter = (
            self.meter.create_observable_up_down_counter(
                "name",
                callbacks=[Mock()],
                unit="unit",
                description="description",
            )
        )
        self.assertIsInstance(
            observable_up_down_counter, ObservableUpDownCounter
        )
        self.assertEqual(observable_up_down_counter.name, "name")

    @patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"})
    def test_get_meter_with_sdk_disabled(self):
        meter_provider = MeterProvider()
        self.assertIsInstance(meter_provider.get_meter(Mock()), NoOpMeter)

    def test_meter_config_default(self):
        config = _MeterConfig.default()
        self.assertTrue(config.is_enabled)

    def test_meter_config_disabled(self):
        config = _MeterConfig(is_enabled=False)
        self.assertFalse(config.is_enabled)

    def test_proxy_meter_config_delegates(self):
        proxy = _ProxyMeterConfig(_MeterConfig(is_enabled=True))
        self.assertTrue(proxy.is_enabled)
        proxy_disabled = _ProxyMeterConfig(_MeterConfig(is_enabled=False))
        self.assertFalse(proxy_disabled.is_enabled)

    def test_proxy_meter_config_update(self):
        proxy = _ProxyMeterConfig(_MeterConfig(is_enabled=True))
        self.assertTrue(proxy.is_enabled)
        proxy.update(_MeterConfig(is_enabled=False))
        self.assertFalse(proxy.is_enabled)
        proxy.update(_MeterConfig(is_enabled=True))
        self.assertTrue(proxy.is_enabled)

    def test_default_meter_configurator(self):
        scope = InstrumentationScope("any_name", "1.0")
        config = _default_meter_configurator(scope)
        self.assertTrue(config.is_enabled)

    def test_disable_meter_configurator(self):
        scope = InstrumentationScope("any_name", "1.0")
        config = _disable_meter_configurator(scope)
        self.assertFalse(config.is_enabled)

    def test_rule_based_configurator_first_match_wins(self):
        disabled_config = _MeterConfig(is_enabled=False)
        enabled_config = _MeterConfig(is_enabled=True)
        configurator = _RuleBasedMeterConfigurator(
            rules=[
                (lambda s: s.name == "foo", disabled_config),
                (lambda s: s.name == "foo", enabled_config),
            ],
            default_config=enabled_config,
        )
        scope = InstrumentationScope("foo", "1.0")
        result = configurator(scope)
        self.assertFalse(result.is_enabled)

    def test_rule_based_configurator_default_when_no_match(self):
        disabled_config = _MeterConfig(is_enabled=False)
        configurator = _RuleBasedMeterConfigurator(
            rules=[
                (
                    lambda s: s.name == "specific",
                    _MeterConfig(is_enabled=True),
                ),
            ],
            default_config=disabled_config,
        )
        scope = InstrumentationScope("other", "1.0")
        result = configurator(scope)
        self.assertFalse(result.is_enabled)

    def test_rule_based_configurator_with_glob_predicate(self):
        disabled_config = _MeterConfig(is_enabled=False)
        configurator = _RuleBasedMeterConfigurator(
            rules=[
                (_scope_name_matches_glob("opentelemetry.*"), disabled_config),
            ],
            default_config=_MeterConfig.default(),
        )
        self.assertFalse(
            configurator(
                InstrumentationScope("opentelemetry.sdk", "1.0")
            ).is_enabled
        )
        self.assertTrue(
            configurator(InstrumentationScope("custom.name", "1.0")).is_enabled
        )

    def test_scope_name_matches_glob_exact(self):
        predicate = _scope_name_matches_glob("my.meter")
        self.assertTrue(predicate(InstrumentationScope("my.meter", "1.0")))

    def test_scope_name_matches_glob_wildcard(self):
        predicate = _scope_name_matches_glob("my.*")
        self.assertTrue(predicate(InstrumentationScope("my.meter", "1.0")))
        self.assertTrue(predicate(InstrumentationScope("my.other", "1.0")))
        self.assertFalse(predicate(InstrumentationScope("other.meter", "1.0")))

    def test_scope_name_matches_glob_no_match(self):
        predicate = _scope_name_matches_glob("no.match")
        self.assertFalse(predicate(InstrumentationScope("my.meter", "1.0")))

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_disabled_meter_counter_skips_measurement(
        self, mock_sync_measurement_consumer
    ):
        sync_consumer_instance = mock_sync_measurement_consumer()
        mp = MeterProvider(_meter_configurator=_disable_meter_configurator)
        counter = mp.get_meter("test").create_counter("c")
        counter.add(1)
        sync_consumer_instance.consume_measurement.assert_not_called()

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_disabled_meter_up_down_counter_skips_measurement(
        self, mock_sync_measurement_consumer
    ):
        sync_consumer_instance = mock_sync_measurement_consumer()
        mp = MeterProvider(_meter_configurator=_disable_meter_configurator)
        counter = mp.get_meter("test").create_up_down_counter("udc")
        counter.add(1)
        sync_consumer_instance.consume_measurement.assert_not_called()

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_disabled_meter_histogram_skips_measurement(
        self, mock_sync_measurement_consumer
    ):
        sync_consumer_instance = mock_sync_measurement_consumer()
        mp = MeterProvider(_meter_configurator=_disable_meter_configurator)
        histogram = mp.get_meter("test").create_histogram("h")
        histogram.record(1)
        sync_consumer_instance.consume_measurement.assert_not_called()

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_disabled_meter_gauge_skips_measurement(
        self, mock_sync_measurement_consumer
    ):
        sync_consumer_instance = mock_sync_measurement_consumer()
        mp = MeterProvider(_meter_configurator=_disable_meter_configurator)
        gauge = mp.get_meter("test").create_gauge("g")
        gauge.set(1)
        sync_consumer_instance.consume_measurement.assert_not_called()

    def test_disabled_meter_observable_counter_skips_callback(self):
        cb = Mock()
        mp = MeterProvider(_meter_configurator=_disable_meter_configurator)
        oc = mp.get_meter("test").create_observable_counter(
            "oc", callbacks=[cb]
        )
        # Trigger callback collection
        list(oc.callback(Mock()))
        cb.assert_not_called()

    def test_disabled_meter_observable_gauge_skips_callback(self):
        cb = Mock()
        mp = MeterProvider(_meter_configurator=_disable_meter_configurator)
        og = mp.get_meter("test").create_observable_gauge("og", callbacks=[cb])
        list(og.callback(Mock()))
        cb.assert_not_called()

    def test_disabled_meter_observable_up_down_counter_skips_callback(self):
        cb = Mock()
        mp = MeterProvider(_meter_configurator=_disable_meter_configurator)
        oudc = mp.get_meter("test").create_observable_up_down_counter(
            "oudc", callbacks=[cb]
        )
        list(oudc.callback(Mock()))
        cb.assert_not_called()

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_counter_noop_after_meter_disabled(
        self, mock_sync_measurement_consumer
    ):
        sync_consumer_instance = mock_sync_measurement_consumer()
        mp = MeterProvider()
        meter = mp.get_meter("test")
        counter = meter.create_counter("c")

        counter.add(1)
        self.assertEqual(
            sync_consumer_instance.consume_measurement.call_count, 1
        )

        counter.add(2)
        self.assertEqual(
            sync_consumer_instance.consume_measurement.call_count, 2
        )

        mp._set_meter_configurator(
            meter_configurator=_disable_meter_configurator
        )
        self.assertFalse(meter._is_enabled())

        counter.add(3)
        counter.add(4)
        self.assertEqual(
            sync_consumer_instance.consume_measurement.call_count, 2
        )

    @patch(
        "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer"
    )
    def test_reenable_meter_after_disable(
        self, mock_sync_measurement_consumer
    ):
        sync_consumer_instance = mock_sync_measurement_consumer()
        mp = MeterProvider(_meter_configurator=_disable_meter_configurator)
        meter = mp.get_meter("test")
        counter = meter.create_counter("c")

        counter.add(1)
        sync_consumer_instance.consume_measurement.assert_not_called()

        mp._set_meter_configurator(
            meter_configurator=_default_meter_configurator
        )
        self.assertTrue(meter._is_enabled())
        counter.add(1)
        sync_consumer_instance.consume_measurement.assert_called_once()


class InMemoryMetricExporter(MetricExporter):
    def __init__(self):
        super().__init__()
        self.metrics = {}
        self._counter = 0

    def export(
        self,
        metrics_data: Sequence[Metric],
        timeout_millis: float = 10_000,
        **kwargs,
    ) -> MetricExportResult:
        self.metrics[self._counter] = metrics_data
        self._counter += 1
        return MetricExportResult.SUCCESS

    def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
        pass

    def force_flush(self, timeout_millis: float = 10_000) -> bool:
        return True


class TestDuplicateInstrumentAggregateData(TestCase):
    def test_duplicate_instrument_aggregate_data(self):
        exporter = InMemoryMetricExporter()
        reader = PeriodicExportingMetricReader(
            exporter, export_interval_millis=500
        )
        view = View(
            instrument_type=Counter,
            attribute_keys=[],
            aggregation=SumAggregation(),
        )
        provider = MeterProvider(
            metric_readers=[reader],
            resource=Resource.create(),
            views=[view],
        )

        meter_0 = provider.get_meter(
            name="meter_0",
            version="version",
            schema_url="schema_url",
        )
        meter_1 = provider.get_meter(
            name="meter_1",
            version="version",
            schema_url="schema_url",
        )
        counter_0_0 = meter_0.create_counter(
            "counter", unit="unit", description="description"
        )
        counter_0_1 = meter_0.create_counter(
            "counter", unit="unit", description="description"
        )
        counter_1_0 = meter_1.create_counter(
            "counter", unit="unit", description="description"
        )

        self.assertIs(counter_0_0, counter_0_1)
        self.assertIsNot(counter_0_0, counter_1_0)

        counter_0_0.add(1, {})
        counter_0_1.add(2, {})

        with self.assertLogs(level=WARNING):
            counter_1_0.add(7, {})

        sleep(1)

        reader.shutdown()

        sleep(1)

        metrics = exporter.metrics[0]

        scope_metrics = metrics.resource_metrics[0].scope_metrics
        self.assertEqual(len(scope_metrics), 2)

        metric_0 = scope_metrics[0].metrics[0]

        self.assertEqual(metric_0.name, "counter")
        self.assertEqual(metric_0.unit, "unit")
        self.assertEqual(metric_0.description, "description")
        self.assertEqual(next(iter(metric_0.data.data_points)).value, 3)

        metric_1 = scope_metrics[1].metrics[0]

        self.assertEqual(metric_1.name, "counter")
        self.assertEqual(metric_1.unit, "unit")
        self.assertEqual(metric_1.description, "description")
        self.assertEqual(next(iter(metric_1.data.data_points)).value, 7)
