From 1d85faa54e7526f3da31abf220e0b51da3742ee8 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 11 Sep 2026 13:52:11 +0200 Subject: [PATCH] feat(data): entity and domain mappers The translation either way, including the repeat mask's bit order and the nullable per-alarm overrides that mean "inherit the app default" rather than a concrete value. Co-Authored-By: Claude Opus 5 --- .../clockula/data/alarms/AlarmMapper.kt | 70 ++++++++++ .../clockula/data/stopwatch/LapMapper.kt | 14 ++ .../clockula/data/timers/TimerMapper.kt | 61 +++++++++ .../data/worldclocks/WorldClockMapper.kt | 26 ++++ .../clockula/data/alarms/AlarmMapperTest.kt | 127 ++++++++++++++++++ .../clockula/data/stopwatch/LapMapperTest.kt | 44 ++++++ .../clockula/data/timers/TimerMapperTest.kt | 97 +++++++++++++ .../data/worldclocks/WorldClockMapperTest.kt | 43 ++++++ .../clockula/testing/Fixtures.kt | 101 ++++++++++++++ 9 files changed, 583 insertions(+) create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapper.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/data/stopwatch/LapMapper.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/data/timers/TimerMapper.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/data/worldclocks/WorldClockMapper.kt create mode 100644 app/src/test/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapperTest.kt create mode 100644 app/src/test/java/de/jeanlucmakiola/clockula/data/stopwatch/LapMapperTest.kt create mode 100644 app/src/test/java/de/jeanlucmakiola/clockula/data/timers/TimerMapperTest.kt create mode 100644 app/src/test/java/de/jeanlucmakiola/clockula/data/worldclocks/WorldClockMapperTest.kt create mode 100644 app/src/test/java/de/jeanlucmakiola/clockula/testing/Fixtures.kt diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapper.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapper.kt new file mode 100644 index 0000000..c5f2933 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapper.kt @@ -0,0 +1,70 @@ +package de.jeanlucmakiola.clockula.data.alarms + +import de.jeanlucmakiola.clockula.domain.Alarm +import de.jeanlucmakiola.clockula.domain.AlarmDraft +import de.jeanlucmakiola.clockula.domain.DismissChallenge +import de.jeanlucmakiola.clockula.domain.RepeatDays +import de.jeanlucmakiola.clockula.domain.TimeOfDay +import de.jeanlucmakiola.floret.prefs.toEnum +import kotlin.time.Instant + +/** Entity ↔ domain. Reads are **forgiving**: a corrupt row degrades, never throws. */ +object AlarmMapper { + fun toDomain(entity: AlarmEntity): Alarm = Alarm( + id = entity.id, + time = TimeOfDay.clamped(entity.hour, entity.minute), + label = entity.label, + enabled = entity.enabled, + repeatDays = RepeatDays.fromMask(entity.repeatDays), + skipNextOccurrence = entity.skipNextOccurrence, + ringtoneUri = entity.ringtoneUri?.takeIf { it.isNotBlank() }, + vibrate = entity.vibrate, + snoozeMinutes = entity.snoozeMinutes, + snoozeLimit = entity.snoozeLimit, + volumeRampSeconds = entity.volumeRampSeconds, + // An absent challenge is an *override that isn't set*, so it stays null; + // a stored name the app no longer knows degrades to NONE. + dismissChallenge = entity.dismissChallenge?.toEnum(DismissChallenge.NONE), + createdAt = Instant.fromEpochMilliseconds(entity.createdAt), + updatedAt = Instant.fromEpochMilliseconds(entity.updatedAt), + ) + + fun toDomain(entities: List): List = entities.map(::toDomain) + + fun toEntity(alarm: Alarm): AlarmEntity = AlarmEntity( + id = alarm.id, + hour = alarm.time.hour, + minute = alarm.time.minute, + label = alarm.label, + enabled = alarm.enabled, + repeatDays = alarm.repeatDays.mask, + skipNextOccurrence = alarm.skipNextOccurrence, + ringtoneUri = alarm.ringtoneUri, + vibrate = alarm.vibrate, + snoozeMinutes = alarm.snoozeMinutes, + snoozeLimit = alarm.snoozeLimit, + volumeRampSeconds = alarm.volumeRampSeconds, + dismissChallenge = alarm.dismissChallenge?.name, + createdAt = alarm.createdAt.toEpochMilliseconds(), + updatedAt = alarm.updatedAt.toEpochMilliseconds(), + ) + + /** id = 0, createdAt = updatedAt = [now]. */ + fun toEntity(draft: AlarmDraft, now: Instant): AlarmEntity = AlarmEntity( + id = 0L, + hour = draft.time.hour, + minute = draft.time.minute, + label = draft.label, + enabled = draft.enabled, + repeatDays = draft.repeatDays.mask, + skipNextOccurrence = false, + ringtoneUri = draft.ringtoneUri, + vibrate = draft.vibrate, + snoozeMinutes = draft.snoozeMinutes, + snoozeLimit = draft.snoozeLimit, + volumeRampSeconds = draft.volumeRampSeconds, + dismissChallenge = draft.dismissChallenge?.name, + createdAt = now.toEpochMilliseconds(), + updatedAt = now.toEpochMilliseconds(), + ) +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/stopwatch/LapMapper.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/stopwatch/LapMapper.kt new file mode 100644 index 0000000..193c3fe --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/stopwatch/LapMapper.kt @@ -0,0 +1,14 @@ +package de.jeanlucmakiola.clockula.data.stopwatch + +import de.jeanlucmakiola.clockula.domain.Lap +import kotlin.time.Duration.Companion.milliseconds + +object LapMapper { + fun toDomain(entity: LapEntity): Lap = Lap( + index = entity.lapIndex, + split = entity.splitMillis.coerceAtLeast(0L).milliseconds, + cumulative = entity.cumulativeMillis.coerceAtLeast(0L).milliseconds, + ) + + fun toDomain(entities: List): List = entities.map(::toDomain) +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/timers/TimerMapper.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/timers/TimerMapper.kt new file mode 100644 index 0000000..2b126a7 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/timers/TimerMapper.kt @@ -0,0 +1,61 @@ +package de.jeanlucmakiola.clockula.data.timers + +import de.jeanlucmakiola.clockula.domain.Timer +import de.jeanlucmakiola.clockula.domain.TimerDraft +import de.jeanlucmakiola.clockula.domain.TimerState +import de.jeanlucmakiola.floret.prefs.toEnum +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Instant + +object TimerMapper { + fun toDomain(entity: TimerEntity): Timer = Timer( + id = entity.id, + label = entity.label, + duration = entity.durationMillis.coerceAtLeast(0L).milliseconds, + state = entity.state.toEnum(TimerState.IDLE), + remaining = entity.remainingMillis.coerceAtLeast(0L).milliseconds, + startedAtElapsedRealtime = entity.startedAtElapsedRealtimeMillis?.milliseconds, + endsAtElapsedRealtime = entity.endsAtElapsedRealtimeMillis?.milliseconds, + endsAtWallClock = entity.endsAtWallClockMillis?.let(Instant::fromEpochMilliseconds), + ringtoneUri = entity.ringtoneUri?.takeIf { it.isNotBlank() }, + sortOrder = entity.sortOrder, + createdAt = Instant.fromEpochMilliseconds(entity.createdAt), + updatedAt = Instant.fromEpochMilliseconds(entity.updatedAt), + ) + + fun toDomain(entities: List): List = entities.map(::toDomain) + + fun toEntity(timer: Timer): TimerEntity = TimerEntity( + id = timer.id, + label = timer.label, + durationMillis = timer.duration.inWholeMilliseconds, + state = timer.state.name, + remainingMillis = timer.remaining.inWholeMilliseconds, + startedAtElapsedRealtimeMillis = timer.startedAtElapsedRealtime?.inWholeMilliseconds, + endsAtElapsedRealtimeMillis = timer.endsAtElapsedRealtime?.inWholeMilliseconds, + endsAtWallClockMillis = timer.endsAtWallClock?.toEpochMilliseconds(), + ringtoneUri = timer.ringtoneUri, + sortOrder = timer.sortOrder, + createdAt = timer.createdAt.toEpochMilliseconds(), + updatedAt = timer.updatedAt.toEpochMilliseconds(), + ) + + fun toEntity(draft: TimerDraft, sortOrder: Int, now: Instant): TimerEntity { + val duration = draft.duration.coerceAtLeast(Duration.ZERO) + return TimerEntity( + id = 0L, + label = draft.label, + durationMillis = duration.inWholeMilliseconds, + state = TimerState.IDLE.name, + remainingMillis = duration.inWholeMilliseconds, + startedAtElapsedRealtimeMillis = null, + endsAtElapsedRealtimeMillis = null, + endsAtWallClockMillis = null, + ringtoneUri = draft.ringtoneUri, + sortOrder = sortOrder, + createdAt = now.toEpochMilliseconds(), + updatedAt = now.toEpochMilliseconds(), + ) + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/worldclocks/WorldClockMapper.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/worldclocks/WorldClockMapper.kt new file mode 100644 index 0000000..e9b599b --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/worldclocks/WorldClockMapper.kt @@ -0,0 +1,26 @@ +package de.jeanlucmakiola.clockula.data.worldclocks + +import de.jeanlucmakiola.clockula.domain.WorldClock + +object WorldClockMapper { + /** + * A zone id the device no longer knows is passed through untouched — + * `WorldClock.isKnownZone` says so and M8 shows the row as broken. Blanking + * it here would silently lose what the user chose. + */ + fun toDomain(entity: WorldClockEntity): WorldClock = WorldClock( + id = entity.id, + zoneId = entity.zoneId, + label = entity.label?.takeIf { it.isNotBlank() }, + sortOrder = entity.sortOrder, + ) + + fun toDomain(entities: List): List = entities.map(::toDomain) + + fun toEntity(clock: WorldClock): WorldClockEntity = WorldClockEntity( + id = clock.id, + zoneId = clock.zoneId, + label = clock.label, + sortOrder = clock.sortOrder, + ) +} diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapperTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapperTest.kt new file mode 100644 index 0000000..59a1667 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapperTest.kt @@ -0,0 +1,127 @@ +package de.jeanlucmakiola.clockula.data.alarms + +import com.google.common.truth.Truth.assertThat +import de.jeanlucmakiola.clockula.domain.Alarm +import de.jeanlucmakiola.clockula.domain.AlarmDraft +import de.jeanlucmakiola.clockula.domain.DismissChallenge +import de.jeanlucmakiola.clockula.domain.RepeatDays +import de.jeanlucmakiola.clockula.domain.TimeOfDay +import de.jeanlucmakiola.clockula.testing.T0 +import de.jeanlucmakiola.clockula.testing.fullAlarm +import de.jeanlucmakiola.clockula.testing.fullAlarmEntity +import org.junit.jupiter.api.Test +import kotlin.time.Instant + +/** + * The mapper is the single place a corrupt row is made harmless: reads degrade, + * they never throw, because a throw inside a cursor read is a crashed list. + */ +class AlarmMapperTest { + + @Test + fun `a fully populated row maps field by field`() { + val alarm = AlarmMapper.toDomain(fullAlarmEntity()) + + assertThat(alarm).isEqualTo( + Alarm( + id = 42L, + time = TimeOfDay(7, 5), + label = "Gym", + enabled = true, + repeatDays = RepeatDays.WEEKDAYS, + skipNextOccurrence = true, + ringtoneUri = "content://media/42", + vibrate = false, + snoozeMinutes = 7, + snoozeLimit = 2, + volumeRampSeconds = 30, + dismissChallenge = DismissChallenge.MATH, + createdAt = Instant.fromEpochMilliseconds(1_700_000_000_000L), + updatedAt = Instant.fromEpochMilliseconds(1_700_000_060_000L), + ), + ) + } + + @Test + fun `a row round-trips through the domain model`() { + val entity = fullAlarmEntity() + + assertThat(AlarmMapper.toEntity(AlarmMapper.toDomain(entity))).isEqualTo(entity) + } + + @Test + fun `an alarm round-trips through the entity`() { + val alarm = fullAlarm() + + assertThat(AlarmMapper.toDomain(AlarmMapper.toEntity(alarm))).isEqualTo(alarm) + } + + @Test + fun `an unknown dismiss challenge degrades to none`() { + val entity = fullAlarmEntity().copy(dismissChallenge = "TELEPORT") + + assertThat(AlarmMapper.toDomain(entity).dismissChallenge).isEqualTo(DismissChallenge.NONE) + } + + @Test + fun `an absent dismiss challenge stays absent`() { + val entity = fullAlarmEntity().copy(dismissChallenge = null) + + assertThat(AlarmMapper.toDomain(entity).dismissChallenge).isNull() + } + + @Test + fun `an empty dismiss challenge degrades to none`() { + val entity = fullAlarmEntity().copy(dismissChallenge = "") + + assertThat(AlarmMapper.toDomain(entity).dismissChallenge).isEqualTo(DismissChallenge.NONE) + } + + @Test + fun `a corrupt repeat mask is sanitised`() { + val entity = fullAlarmEntity().copy(repeatDays = 255) + + assertThat(AlarmMapper.toDomain(entity).repeatDays.mask).isEqualTo(127) + } + + @Test + fun `an out-of-range time is clamped rather than thrown`() { + val entity = fullAlarmEntity().copy(hour = 25, minute = 88) + + assertThat(AlarmMapper.toDomain(entity).time).isEqualTo(TimeOfDay(23, 59)) + } + + @Test + fun `a blank ringtone uri reads as absent`() { + val entity = fullAlarmEntity().copy(ringtoneUri = "") + + assertThat(AlarmMapper.toDomain(entity).ringtoneUri).isNull() + } + + @Test + fun `a draft becomes a new row stamped with the given instant`() { + val draft = AlarmDraft(TimeOfDay(6, 0), label = "Work", repeatDays = RepeatDays.WEEKDAYS) + + val entity = AlarmMapper.toEntity(draft, T0) + + assertThat(entity).isEqualTo( + AlarmEntity( + id = 0L, + hour = 6, + minute = 0, + label = "Work", + enabled = true, + repeatDays = 31, + skipNextOccurrence = false, + ringtoneUri = null, + vibrate = null, + snoozeMinutes = null, + snoozeLimit = null, + volumeRampSeconds = null, + dismissChallenge = null, + createdAt = T0.toEpochMilliseconds(), + updatedAt = T0.toEpochMilliseconds(), + ), + ) + } +} diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/stopwatch/LapMapperTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/stopwatch/LapMapperTest.kt new file mode 100644 index 0000000..cfb12b3 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/stopwatch/LapMapperTest.kt @@ -0,0 +1,44 @@ +package de.jeanlucmakiola.clockula.data.stopwatch + +import com.google.common.truth.Truth.assertThat +import de.jeanlucmakiola.clockula.domain.Lap +import de.jeanlucmakiola.clockula.testing.lapEntity +import org.junit.jupiter.api.Test +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +class LapMapperTest { + + @Test + fun `a row maps field by field, without its row id`() { + val lap = LapMapper.toDomain(lapEntity()) + + assertThat(lap).isEqualTo(Lap(index = 3, split = 1_234.milliseconds, cumulative = 5_678.milliseconds)) + } + + @Test + fun `a list maps in the order given`() { + val entities = listOf( + LapEntity(id = 1L, lapIndex = 1, splitMillis = 1_000L, cumulativeMillis = 1_000L), + LapEntity(id = 2L, lapIndex = 2, splitMillis = 2_000L, cumulativeMillis = 3_000L), + LapEntity(id = 3L, lapIndex = 3, splitMillis = 3_000L, cumulativeMillis = 6_000L), + ) + + val laps = LapMapper.toDomain(entities) + + assertThat(laps.map { it.index }).containsExactly(1, 2, 3).inOrder() + } + + @Test + fun `negative millis are clamped to zero`() { + val lap = LapMapper.toDomain(lapEntity().copy(splitMillis = -1L, cumulativeMillis = -1L)) + + assertThat(lap.split).isEqualTo(Duration.ZERO) + assertThat(lap.cumulative).isEqualTo(Duration.ZERO) + } + + @Test + fun `an empty list maps to an empty list`() { + assertThat(LapMapper.toDomain(emptyList())).isEmpty() + } +} diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/timers/TimerMapperTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/timers/TimerMapperTest.kt new file mode 100644 index 0000000..6a23110 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/timers/TimerMapperTest.kt @@ -0,0 +1,97 @@ +package de.jeanlucmakiola.clockula.data.timers + +import com.google.common.truth.Truth.assertThat +import de.jeanlucmakiola.clockula.domain.TimerDraft +import de.jeanlucmakiola.clockula.domain.TimerState +import de.jeanlucmakiola.clockula.testing.T0 +import de.jeanlucmakiola.clockula.testing.runningTimer +import de.jeanlucmakiola.clockula.testing.runningTimerEntity +import org.junit.jupiter.api.Test +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +class TimerMapperTest { + + @Test + fun `a running row maps field by field`() { + val timer = TimerMapper.toDomain(runningTimerEntity()) + + assertThat(timer).isEqualTo(runningTimer()) + } + + @Test + fun `a row round-trips through the domain model`() { + val entity = runningTimerEntity() + + assertThat(TimerMapper.toEntity(TimerMapper.toDomain(entity))).isEqualTo(entity) + } + + @Test + fun `a timer round-trips through the entity`() { + val timer = runningTimer() + + assertThat(TimerMapper.toDomain(TimerMapper.toEntity(timer))).isEqualTo(timer) + } + + @Test + fun `an unknown state degrades to idle`() { + val entity = runningTimerEntity().copy(state = "SPINNING") + + val timer = TimerMapper.toDomain(entity) + + assertThat(timer.state).isEqualTo(TimerState.IDLE) + assertThat(timer.remaining).isEqualTo(200.seconds) + } + + @Test + fun `an idle row has no anchors and its full remaining time`() { + val entity = runningTimerEntity().copy( + state = "IDLE", + remainingMillis = 300_000L, + startedAtElapsedRealtimeMillis = null, + endsAtElapsedRealtimeMillis = null, + endsAtWallClockMillis = null, + ) + + val timer = TimerMapper.toDomain(entity) + + assertThat(timer.startedAtElapsedRealtime).isNull() + assertThat(timer.endsAtElapsedRealtime).isNull() + assertThat(timer.endsAtWallClock).isNull() + assertThat(timer.remaining).isEqualTo(timer.duration) + } + + @Test + fun `a negative duration is clamped to zero`() { + val entity = runningTimerEntity().copy(durationMillis = -5L, remainingMillis = -5L) + + val timer = TimerMapper.toDomain(entity) + + assertThat(timer.duration).isEqualTo(Duration.ZERO) + assertThat(timer.remaining).isEqualTo(Duration.ZERO) + } + + @Test + fun `a draft becomes a new idle row`() { + val draft = TimerDraft(90.seconds, label = "Tea") + + val entity = TimerMapper.toEntity(draft, sortOrder = 2, now = T0) + + assertThat(entity).isEqualTo( + TimerEntity( + id = 0L, + label = "Tea", + durationMillis = 90_000L, + state = "IDLE", + remainingMillis = 90_000L, + startedAtElapsedRealtimeMillis = null, + endsAtElapsedRealtimeMillis = null, + endsAtWallClockMillis = null, + ringtoneUri = null, + sortOrder = 2, + createdAt = T0.toEpochMilliseconds(), + updatedAt = T0.toEpochMilliseconds(), + ), + ) + } +} diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/worldclocks/WorldClockMapperTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/worldclocks/WorldClockMapperTest.kt new file mode 100644 index 0000000..e517c26 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/worldclocks/WorldClockMapperTest.kt @@ -0,0 +1,43 @@ +package de.jeanlucmakiola.clockula.data.worldclocks + +import com.google.common.truth.Truth.assertThat +import de.jeanlucmakiola.clockula.domain.WorldClock +import de.jeanlucmakiola.clockula.domain.isKnownZone +import de.jeanlucmakiola.clockula.testing.worldClockEntity +import org.junit.jupiter.api.Test + +class WorldClockMapperTest { + + @Test + fun `a row maps field by field`() { + val clock = WorldClockMapper.toDomain(worldClockEntity()) + + assertThat(clock).isEqualTo(WorldClock(3L, "Europe/Berlin", "Home", 2)) + } + + @Test + fun `a row round-trips through the domain model`() { + val entity = worldClockEntity() + + assertThat(WorldClockMapper.toEntity(WorldClockMapper.toDomain(entity))).isEqualTo(entity) + } + + @Test + fun `a blank label reads as absent`() { + assertThat(WorldClockMapper.toDomain(worldClockEntity().copy(label = "")).label).isNull() + assertThat(WorldClockMapper.toDomain(worldClockEntity().copy(label = " ")).label).isNull() + } + + @Test + fun `a zone id the device no longer knows is preserved, not blanked`() { + val entity = worldClockEntity().copy(zoneId = "Mars/Phobos") + + assertThat(WorldClockMapper.toDomain(entity).zoneId).isEqualTo("Mars/Phobos") + } + + @Test + fun `isKnownZone reflects the device tzdata`() { + assertThat(WorldClock(1L, "Europe/Berlin", null, 0).isKnownZone).isTrue() + assertThat(WorldClock(1L, "Mars/Phobos", null, 0).isKnownZone).isFalse() + } +} diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/testing/Fixtures.kt b/app/src/test/java/de/jeanlucmakiola/clockula/testing/Fixtures.kt new file mode 100644 index 0000000..bf751cc --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/clockula/testing/Fixtures.kt @@ -0,0 +1,101 @@ +package de.jeanlucmakiola.clockula.testing + +import de.jeanlucmakiola.clockula.data.alarms.AlarmEntity +import de.jeanlucmakiola.clockula.data.stopwatch.LapEntity +import de.jeanlucmakiola.clockula.data.timers.TimerEntity +import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockEntity +import de.jeanlucmakiola.clockula.domain.Alarm +import de.jeanlucmakiola.clockula.domain.DismissChallenge +import de.jeanlucmakiola.clockula.domain.RepeatDays +import de.jeanlucmakiola.clockula.domain.TimeOfDay +import de.jeanlucmakiola.clockula.domain.Timer +import de.jeanlucmakiola.clockula.domain.TimerState +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.Instant + +/** The one wall-clock instant every fixture is anchored on. */ +val T0: Instant = Instant.fromEpochMilliseconds(1_700_000_000_000L) + +/** + * Every field a distinct, non-default value, so a mapper that crosses two + * fields over cannot pass by coincidence. + */ +fun fullAlarmEntity(): AlarmEntity = AlarmEntity( + id = 42L, + hour = 7, + minute = 5, + label = "Gym", + enabled = true, + repeatDays = 31, + skipNextOccurrence = true, + ringtoneUri = "content://media/42", + vibrate = false, + snoozeMinutes = 7, + snoozeLimit = 2, + volumeRampSeconds = 30, + dismissChallenge = "MATH", + createdAt = 1_700_000_000_000L, + updatedAt = 1_700_000_060_000L, +) + +fun fullAlarm(): Alarm = Alarm( + id = 42L, + time = TimeOfDay(7, 5), + label = "Gym", + enabled = true, + repeatDays = RepeatDays.WEEKDAYS, + skipNextOccurrence = true, + ringtoneUri = "content://media/42", + vibrate = false, + snoozeMinutes = 7, + snoozeLimit = 2, + volumeRampSeconds = 30, + dismissChallenge = DismissChallenge.MATH, + createdAt = Instant.fromEpochMilliseconds(1_700_000_000_000L), + updatedAt = Instant.fromEpochMilliseconds(1_700_000_060_000L), +) + +fun runningTimerEntity(): TimerEntity = TimerEntity( + id = 7L, + label = "Pasta", + durationMillis = 300_000L, + state = "RUNNING", + remainingMillis = 200_000L, + startedAtElapsedRealtimeMillis = 1_000_000L, + endsAtElapsedRealtimeMillis = 1_300_000L, + endsAtWallClockMillis = 1_700_000_300_000L, + ringtoneUri = "content://media/7", + sortOrder = 3, + createdAt = 1_700_000_000_000L, + updatedAt = 1_700_000_060_000L, +) + +fun runningTimer(): Timer = Timer( + id = 7L, + label = "Pasta", + duration = 5.minutes, + state = TimerState.RUNNING, + remaining = 200.seconds, + startedAtElapsedRealtime = 1_000.seconds, + endsAtElapsedRealtime = 1_300.seconds, + endsAtWallClock = Instant.fromEpochMilliseconds(1_700_000_300_000L), + ringtoneUri = "content://media/7", + sortOrder = 3, + createdAt = Instant.fromEpochMilliseconds(1_700_000_000_000L), + updatedAt = Instant.fromEpochMilliseconds(1_700_000_060_000L), +) + +fun worldClockEntity(): WorldClockEntity = WorldClockEntity( + id = 3L, + zoneId = "Europe/Berlin", + label = "Home", + sortOrder = 2, +) + +fun lapEntity(): LapEntity = LapEntity( + id = 9L, + lapIndex = 3, + splitMillis = 1_234L, + cumulativeMillis = 5_678L, +)