package eu.darken.bluemusic.monitor.core.modules.volume

import android.media.AudioManager
import eu.darken.bluemusic.bluetooth.core.SourceDevice
import eu.darken.bluemusic.common.time.MonotonicClock
import eu.darken.bluemusic.devices.core.DeviceRepo
import eu.darken.bluemusic.devices.core.ManagedDevice
import eu.darken.bluemusic.devices.core.database.DeviceConfigEntity
import eu.darken.bluemusic.monitor.core.audio.AudioStream
import eu.darken.bluemusic.monitor.core.audio.VolumeEvent
import eu.darken.bluemusic.monitor.core.audio.VolumeTool
import io.kotest.matchers.shouldBe
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.time.FakeMonotonicClock

class VolumeRateLimiterModuleTest : BaseTest() {

    private val address = "AA:BB:CC:DD:EE:FF"

    private lateinit var clock: FakeMonotonicClock
    private lateinit var volumeTool: VolumeTool
    private lateinit var deviceRepo: DeviceRepo
    private lateinit var ownerRegistry: eu.darken.bluemusic.monitor.core.ownership.AudioStreamOwnerRegistry
    private lateinit var sourceDevice: SourceDevice
    private lateinit var devicesFlow: MutableStateFlow<List<ManagedDevice>>

    @BeforeEach
    fun setup() {
        clock = FakeMonotonicClock(now = 1000L)
        volumeTool = mockk(relaxed = true)
        deviceRepo = mockk(relaxed = true)
        ownerRegistry = eu.darken.bluemusic.monitor.core.ownership.AudioStreamOwnerRegistry()
        devicesFlow = MutableStateFlow(emptyList())
        every { deviceRepo.devices } returns devicesFlow

        sourceDevice = mockk {
            every { this@mockk.address } returns this@VolumeRateLimiterModuleTest.address
            every { label } returns "Test Device"
            every { deviceType } returns SourceDevice.Type.HEADPHONES
            every { getStreamId(AudioStream.Type.MUSIC) } returns AudioStream.Id.STREAM_MUSIC
            every { getStreamId(AudioStream.Type.CALL) } returns AudioStream.Id.STREAM_VOICE_CALL
            every { getStreamId(AudioStream.Type.RINGTONE) } returns AudioStream.Id.STREAM_RINGTONE
            every { getStreamId(AudioStream.Type.NOTIFICATION) } returns AudioStream.Id.STREAM_NOTIFICATION
            every { getStreamId(AudioStream.Type.ALARM) } returns AudioStream.Id.STREAM_ALARM
        }
    }

    private fun createModule() = VolumeRateLimiterModule(
        volumeTool = volumeTool,
        deviceRepo = deviceRepo,
        ownerRegistry = ownerRegistry,
        clock = clock,
    )

    private fun config(
        volumeRateLimiter: Boolean = true,
        volumeLock: Boolean = false,
        isEnabled: Boolean = true,
        musicVolume: Float? = null,
        volumeRateLimitIncreaseMs: Long? = null,
        volumeRateLimitDecreaseMs: Long? = null,
    ): DeviceConfigEntity = DeviceConfigEntity(
        address = address,
        volumeRateLimiter = volumeRateLimiter,
        volumeLock = volumeLock,
        isEnabled = isEnabled,
        musicVolume = musicVolume,
        volumeRateLimitIncreaseMs = volumeRateLimitIncreaseMs,
        volumeRateLimitDecreaseMs = volumeRateLimitDecreaseMs,
    )

    private fun managedDevice(config: DeviceConfigEntity) = ManagedDevice(
        isConnected = true,
        device = sourceDevice,
        config = config,
    )

    private suspend fun seedActive(device: ManagedDevice) {
        devicesFlow.value = listOf(device)
        ownerRegistry.onDeviceConnected(
            address = device.address,
            label = device.label,
            deviceType = device.type,
            receivedAtElapsedMs = 1000L,
            sequence = 0L,
        )
    }

    private fun groupedSource(addr: String, name: String): SourceDevice = mockk {
        every { this@mockk.address } returns addr
        every { label } returns name
        every { deviceType } returns SourceDevice.Type.HEADPHONES
        every { getStreamId(AudioStream.Type.MUSIC) } returns AudioStream.Id.STREAM_MUSIC
        every { getStreamId(AudioStream.Type.CALL) } returns AudioStream.Id.STREAM_VOICE_CALL
        every { getStreamId(AudioStream.Type.RINGTONE) } returns AudioStream.Id.STREAM_RINGTONE
        every { getStreamId(AudioStream.Type.NOTIFICATION) } returns AudioStream.Id.STREAM_NOTIFICATION
        every { getStreamId(AudioStream.Type.ALARM) } returns AudioStream.Id.STREAM_ALARM
    }

    private fun groupedDevice(
        addr: String,
        increaseMs: Long? = null,
        decreaseMs: Long? = null,
        rateLimiter: Boolean = true,
        volumeLock: Boolean = false,
    ): ManagedDevice = ManagedDevice(
        isConnected = true,
        device = groupedSource(addr, "Buds3 Pro"),
        config = DeviceConfigEntity(
            address = addr,
            volumeRateLimiter = rateLimiter,
            volumeLock = volumeLock,
            isEnabled = true,
            musicVolume = 0.5f,
            volumeRateLimitIncreaseMs = increaseMs,
            volumeRateLimitDecreaseMs = decreaseMs,
        ),
    )

    /** Same label + type, connects 2ms apart → one owner group (grouped earbuds). */
    private suspend fun seedGroup(devices: List<ManagedDevice>) {
        devicesFlow.value = devices
        devices.forEachIndexed { index, device ->
            ownerRegistry.onDeviceConnected(
                address = device.address,
                label = device.label,
                deviceType = device.type,
                receivedAtElapsedMs = 1000L + index * 2,
                sequence = index.toLong(),
            )
        }
    }

    @Test
    fun `volume change within rate window is reverted`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 6, self = false))
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 6, newVolume = 10, self = false))

        coVerify { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
    }

    @Test
    fun `rate limiting expires on monotonic clock boundary`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f, volumeRateLimitIncreaseMs = 500L)
        seedActive(managedDevice(cfg))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 6, self = false))

        clock.now = 1499L
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 6, newVolume = 7, self = false))

        clock.now = 1999L
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 6, newVolume = 7, self = false))

        coVerify(exactly = 1) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
    }

    @Test
    fun `first event bypasses rate limiting and applies step limit`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 8, self = false))

        coVerify { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
    }

    @Test
    fun `step limiting clamps increase of 2 to 1`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 7, self = false))

        coVerify { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
    }

    @Test
    fun `step limiting clamps decrease of 2 to 1`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 10, newVolume = 8, self = false))

        coVerify { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 9) }
    }

    @Test
    fun `self-triggered change updates reference without reverting`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 8, self = true))

        coVerify(exactly = 0) { volumeTool.changeVolume(streamId = any(), targetLevel = any()) }
    }

    @Test
    fun `state keyed by streamId only`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 8, self = false))

        module.handle(VolumeEvent(AudioStream.Id.STREAM_RINGTONE, oldVolume = 5, newVolume = 8, self = false))

        coVerify { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
        coVerify { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_RINGTONE, targetLevel = 6) }
    }

    @Test
    fun `rate limiting only applied to owner group devices`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        // Add a second non-owner device with rate limiter enabled
        val secondAddr = "BB:CC:DD:EE:FF:00"
        val secondSource: SourceDevice = mockk {
            every { this@mockk.address } returns secondAddr
            every { label } returns "Other Device"
            every { deviceType } returns SourceDevice.Type.HEADPHONES
            every { getStreamId(AudioStream.Type.MUSIC) } returns AudioStream.Id.STREAM_MUSIC
        }
        val secondCfg = DeviceConfigEntity(address = secondAddr, volumeRateLimiter = true, isEnabled = true, musicVolume = 0.5f)
        devicesFlow.value = devicesFlow.value + ManagedDevice(isConnected = true, device = secondSource, config = secondCfg)
        // Don't register secondAddr in registry — it's not an owner

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 8, self = false))

        // Rate limiting applied (step clamp from 5→8 to 5→6), proving the first device IS eligible
        coVerify { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
    }

    @Test
    fun `ownership change clears rate limiter state`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        // First event sets up state
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 6, self = false))

        // Change ownership by connecting a new device that becomes owner
        ownerRegistry.onDeviceConnected("NEW:DEVICE:00:00:00:01", "NewOwner", SourceDevice.Type.HEADPHONES, 9000L, 10L)

        // New event after ownership change — state should have been cleared
        // Since state is cleared, the new event is treated as initial (no rate limit applied, but step limit applies)
        val newSource: SourceDevice = mockk {
            every { this@mockk.address } returns "NEW:DEVICE:00:00:00:01"
            every { label } returns "NewOwner"
            every { deviceType } returns SourceDevice.Type.HEADPHONES
            every { getStreamId(AudioStream.Type.MUSIC) } returns AudioStream.Id.STREAM_MUSIC
        }
        val newCfg = DeviceConfigEntity(address = "NEW:DEVICE:00:00:00:01", volumeRateLimiter = true, isEnabled = true, musicVolume = 0.5f)
        devicesFlow.value = listOf(ManagedDevice(isConnected = true, device = newSource, config = newCfg))

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 8, self = false))

        // Step limit clamps 5→8 to 5→6 (fresh state, no rate window carry-over)
        coVerify { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
    }

    @Test
    fun `new owner does not inherit previous owner reference volume`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        // Establish reference at volume 10
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 9, newVolume = 10, self = false))

        // Change ownership
        ownerRegistry.onDeviceConnected("NEW:DEVICE:00:00:00:01", "NewOwner", SourceDevice.Type.HEADPHONES, 9000L, 10L)
        val newSource: SourceDevice = mockk {
            every { this@mockk.address } returns "NEW:DEVICE:00:00:00:01"
            every { label } returns "NewOwner"
            every { deviceType } returns SourceDevice.Type.HEADPHONES
            every { getStreamId(AudioStream.Type.MUSIC) } returns AudioStream.Id.STREAM_MUSIC
        }
        devicesFlow.value = listOf(ManagedDevice(isConnected = true, device = newSource, config = DeviceConfigEntity(
            address = "NEW:DEVICE:00:00:00:01", volumeRateLimiter = true, isEnabled = true, musicVolume = 0.5f,
        )))

        // New owner event: old→5, new→6. Without state clear, this would be
        // compared against reference=10 and rate-limited. With cleared state,
        // it's treated as initial with step limit only.
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 6, self = false))

        // Volume 6 is only 1 step from old=5, so no clamping needed — should be accepted
        // (no changeVolume call means it was accepted as-is)
        coVerify(exactly = 0) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 5) }
    }

    @Test
    fun `state keyed includes ownerKey - different owners get separate state`() = runTest {
        val module = createModule()
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        // First owner: establish reference
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 6, self = false))

        // Switch owner
        ownerRegistry.onDeviceConnected("NEW:DEVICE:00:00:00:01", "NewOwner", SourceDevice.Type.HEADPHONES, 9000L, 10L)
        val newSource: SourceDevice = mockk {
            every { this@mockk.address } returns "NEW:DEVICE:00:00:00:01"
            every { label } returns "NewOwner"
            every { deviceType } returns SourceDevice.Type.HEADPHONES
            every { getStreamId(AudioStream.Type.MUSIC) } returns AudioStream.Id.STREAM_MUSIC
        }
        devicesFlow.value = listOf(ManagedDevice(isConnected = true, device = newSource, config = DeviceConfigEntity(
            address = "NEW:DEVICE:00:00:00:01", volumeRateLimiter = true, isEnabled = true, musicVolume = 0.5f,
        )))

        // New owner gets a fresh start — 3→7 triggers step limit (clamp to +1)
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 3, newVolume = 7, self = false))

        // Should clamp to 3+1=4, proving new owner has fresh state (reference=3)
        coVerify { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 4) }
    }

    @Test
    fun `no eligible devices is no-op`() = runTest {
        val module = createModule()
        val cfg = config(volumeRateLimiter = false)
        seedActive(managedDevice(cfg))

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 10, self = false))

        coVerify(exactly = 0) { volumeTool.changeVolume(streamId = any(), targetLevel = any()) }
    }

    @Test
    fun `volume lock overrides rate limiter at runtime`() = runTest {
        val module = createModule()
        val cfg = config(volumeRateLimiter = true, volumeLock = true, musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 10, self = false))

        coVerify(exactly = 0) { volumeTool.changeVolume(streamId = any(), targetLevel = any()) }
    }

    // ------------------------------------------------------------------------
    // Owner-group decisions: one decision per stream/event, most restrictive
    // window wins, independent of repository iteration order.
    // ------------------------------------------------------------------------

    @Test
    fun `grouped 0ms member cannot double-step past sibling window - either order`() = runTest {
        for (zeroFirst in listOf(true, false)) {
            setup()
            val zero = groupedDevice("AA:BB:CC:DD:EE:01", increaseMs = 0L)
            val slow = groupedDevice("AA:BB:CC:DD:EE:02", increaseMs = 1000L)
            seedGroup(if (zeroFirst) listOf(zero, slow) else listOf(slow, zero))

            coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

            val module = createModule()
            module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 15, self = false))

            // Exactly one correction to reference+1 — no second pass stepping to 7
            coVerify(exactly = 1) { volumeTool.changeVolume(streamId = any(), targetLevel = any()) }
            coVerify(exactly = 1) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
        }
    }

    @Test
    fun `grouped devices - longest window governs regardless of order`() = runTest {
        for (fastFirst in listOf(true, false)) {
            setup()
            val fast = groupedDevice("AA:BB:CC:DD:EE:01", increaseMs = 500L)
            val slow = groupedDevice("AA:BB:CC:DD:EE:02", increaseMs = 1000L)
            seedGroup(if (fastFirst) listOf(fast, slow) else listOf(slow, fast))

            coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

            val module = createModule()
            // Allowed single step establishes state at t=1000
            module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 6, self = false))

            // 700ms later: past fast's 500ms window, within slow's 1000ms window
            clock.now = 1700L
            module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 6, newVolume = 15, self = false))

            // Most restrictive window applies → single revert to 6, never a step to 7
            coVerify(exactly = 1) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
            coVerify(exactly = 0) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 7) }
        }
    }

    @Test
    fun `ineligible group member does not influence the group window`() = runTest {
        val zero = groupedDevice("AA:BB:CC:DD:EE:01", increaseMs = 0L)
        // Locked sibling has a long window, but volumeLock makes it rate-limiter-ineligible
        val locked = groupedDevice("AA:BB:CC:DD:EE:02", increaseMs = 1000L, volumeLock = true)
        seedGroup(listOf(zero, locked))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        val module = createModule()
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 6, self = false))
        // Same clock time: with a 0ms window the jump step-clamps instead of reverting
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 6, newVolume = 15, self = false))

        coVerify(exactly = 1) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 7) }
    }

    @Test
    fun `all-zero group acts as pure step limiter - one step per event`() = runTest {
        val a = groupedDevice("AA:BB:CC:DD:EE:01", increaseMs = 0L)
        val b = groupedDevice("AA:BB:CC:DD:EE:02", increaseMs = 0L)
        seedGroup(listOf(a, b))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        val module = createModule()
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 5, newVolume = 15, self = false))
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 6, newVolume = 15, self = false))

        coVerify(exactly = 1) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 6) }
        coVerify(exactly = 1) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 7) }
        coVerify(exactly = 2) { volumeTool.changeVolume(streamId = any(), targetLevel = any()) }
    }

    @Test
    fun `decrease direction aggregates decrease windows not increase windows`() = runTest {
        // Tiny increase windows would (wrongly) let the drop through if the
        // aggregation picked the wrong property for a decrease
        val a = groupedDevice("AA:BB:CC:DD:EE:01", increaseMs = 50L, decreaseMs = 500L)
        val b = groupedDevice("AA:BB:CC:DD:EE:02", increaseMs = 50L, decreaseMs = 1000L)
        seedGroup(listOf(a, b))

        coEvery { volumeTool.changeVolume(streamId = any(), targetLevel = any()) } returns true

        val module = createModule()
        // Allowed single-step decrease establishes state at t=1000
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 10, newVolume = 9, self = false))

        // 700ms later: past both increase windows and a's decrease window, within b's 1000ms decrease window
        clock.now = 1700L
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 9, newVolume = 0, self = false))

        coVerify(exactly = 1) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 9) }
        coVerify(exactly = 0) { volumeTool.changeVolume(streamId = AudioStream.Id.STREAM_MUSIC, targetLevel = 8) }
    }

    @Test
    fun `late observer event for self write is not reverted`() = runTest {
        // Synthesis: real VolumeTool with a fake clock, simulating a ~600ms
        // ContentObserver delay after setStreamVolume. Proves the TTL is wide
        // enough and the observer-time wasUs classification flows into the module.
        val audioManager = mockk<AudioManager>(relaxed = true)
        every { audioManager.getStreamMaxVolume(any()) } returns 15
        every { audioManager.getStreamVolume(any()) } returns 2
        var fakeTime = 1000L
        val realVolumeTool = VolumeTool(audioManager).apply {
            clock = { fakeTime }
        }
        val module = VolumeRateLimiterModule(
            volumeTool = realVolumeTool,
            deviceRepo = deviceRepo,
            ownerRegistry = ownerRegistry,
            clock = object : MonotonicClock {
                override fun nowMs(): Long = fakeTime
            },
        )
        val cfg = config(musicVolume = 0.5f)
        seedActive(managedDevice(cfg))

        realVolumeTool.changeVolume(AudioStream.Id.STREAM_MUSIC, targetLevel = 11)
        fakeTime += 600 // observer fires 600ms after the app write

        val isSelf = realVolumeTool.wasUs(AudioStream.Id.STREAM_MUSIC, 11)
        module.handle(VolumeEvent(AudioStream.Id.STREAM_MUSIC, oldVolume = 2, newVolume = 11, self = isSelf))

        // TTL=2000ms; wasUs at t0+600ms still true → event.self=true → early return, no revert.
        // Only the test's own setVolume(11) reached the AudioManager.
        isSelf shouldBe true
        verify(exactly = 1) { audioManager.setStreamVolume(AudioStream.Id.STREAM_MUSIC.id, 11, 0) }
        verify(exactly = 0) { audioManager.setStreamVolume(AudioStream.Id.STREAM_MUSIC.id, match { it != 11 }, any()) }
    }
}
