package com.battery.device.simulator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Instant; import org.junit.jupiter.api.Test; import com.battery.device.model.DeviceWorkStatus; import com.battery.device.bts.BtsDeviceAdapter; import com.battery.device.bts.BtsTcpTransport; import com.battery.device.bts.BtsXmlCodec; import com.battery.device.simulator.BtsSimulatorStateStore.BtsPlaybackState; import com.battery.device.simulator.InMemoryBtsSimulatorStateStore; /** Ticket21:检测柜 record、step、cycle 生命周期和播放控制合同。 */ class BtsDevelopmentSimulatorRecordStepCycleTest { @Test void recordsUseMinuteDeviceTimeAndActualStepCycleIdentity() throws Exception { Instant start = Instant.parse("2026-08-31T00:00:00Z"); try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore())) { simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING); simulator.advanceBts(start); simulator.advanceBts(start.plusSeconds(5 * 60L)); var records = simulator.btsRecords(1, 1); assertTrue(records.size() >= 6); assertEquals(1L, records.get(0).sequenceId()); assertEquals(60L, records.get(1).deviceTime().getEpochSecond() - records.get(0).deviceTime().getEpochSecond()); assertEquals(1, records.get(0).cycleNo()); assertTrue(simulator.btsSteps(1, 1).stream().anyMatch(step -> step.stepNo() == 1 && step.recordNo() != null && !step.recordNo().isBlank())); var cycle = simulator.btsCycles(1, 1).stream().filter(item -> item.cycleNo() == 1).findFirst().orElseThrow(); assertTrue(cycle.chargeCapacityAh() >= 0d); assertTrue(cycle.dischargeCapacityAh() >= 0d); assertEquals(cycle.chargeCapacityAh() > 0d ? cycle.dischargeCapacityAh() / cycle.chargeCapacityAh() * 100d : 0d, cycle.efficiency(), 0.000001d); } } @Test void testSnapshotExposesPlanRowsEvenBeforeRecordsAreProduced() throws Exception { try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore())) { var snapshot = simulator.btsTest(1, 1); assertTrue(snapshot.plannedCycleCount() > 0); assertTrue(snapshot.rows().stream().anyMatch(row -> "STEP".equals(row.rowType()))); assertTrue(snapshot.rows().stream().anyMatch(row -> "CYCLE".equals(row.rowType()))); assertTrue(snapshot.rows().stream().anyMatch(row -> "END".equals(row.rowType()))); assertEquals(0, simulator.btsRecords(1, 1).size()); } } @Test void testSnapshotKeepsCurrentStepVisibleWhenPlaybackEntersNextCycle() throws Exception { InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(); try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store)) { simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING); simulator.nextBtsCycle(1, 1); var snapshot = simulator.btsTest(1, 1); var expectedSteps = BtsExcelFixture.recordsForCycle(2).stream() .map(BtsExcelFixture.FixtureRecord::stepNo).distinct().toList(); var actualSteps = snapshot.rows().stream().filter(row -> "STEP".equals(row.rowType())) .map(BtsDevelopmentSimulator.BtsTestPlanRow::stepNo).toList(); var cyclePositions = snapshot.rows().stream().filter(row -> "STEP".equals(row.rowType())) .map(BtsDevelopmentSimulator.BtsTestPlanRow::cyclePosition).toList(); assertEquals(expectedSteps, actualSteps); assertEquals(java.util.stream.IntStream.rangeClosed(1, expectedSteps.size()).boxed().toList(), cyclePositions); assertTrue(snapshot.rows().stream().anyMatch(row -> "STEP".equals(row.rowType()) && "进行中".equals(row.executionStatus()))); assertTrue(snapshot.rows().stream().anyMatch(row -> "STEP".equals(row.rowType()) && "待执行".equals(row.executionStatus()))); var cycleRow = snapshot.rows().stream().filter(row -> "CYCLE".equals(row.rowType())).findFirst().orElseThrow(); assertEquals(2, cycleRow.stepNo()); assertEquals(2, cycleRow.cycleCount()); } } @Test void forcedFirstCycleKeepsUnstartedTestPlanIndependentOfSharedPlayback() throws Exception { InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(); try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store)) { simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING); simulator.nextBtsCycle(1, 1); simulator.nextBtsCycle(1, 1); var snapshot = simulator.btsTest(1, 1, 1); assertEquals(1, snapshot.currentCycleNo()); assertTrue(snapshot.rows().stream().filter(row -> "STEP".equals(row.rowType())) .allMatch(row -> "待执行".equals(row.executionStatus()))); } } @Test void playbackSpeedPauseResumeAndRestartAreIndependentOfDeviceStatus() throws Exception { Instant start = Instant.parse("2026-08-31T00:00:00Z"); InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(); try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store)) { simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING); simulator.setBtsSpeed(1, 1, 600); simulator.advanceBts(start); simulator.pauseBts(1, 1); int pausedCount = simulator.btsRecords(1, 1).size(); simulator.advanceBts(start.plusSeconds(600)); assertEquals(pausedCount, simulator.btsRecords(1, 1).size()); assertFalse(simulator.btsPlayback(1, 1).playback().playing()); } try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store)) { BtsPlaybackState playback = restored.btsPlayback(1, 1).playback(); assertEquals(600, playback.speedMultiplier()); assertFalse(playback.playing()); restored.resumeBts(1, 1); assertTrue(restored.btsPlayback(1, 1).playback().playing()); } } @Test void nextCycleAndResetDoNotSendDeviceControlAndNaturalFinishStopsPlayback() throws Exception { InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(); try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store)) { simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING); simulator.nextBtsCycle(1, 1); assertEquals(2, simulator.btsPlayback(1, 1).playback().cycleNo()); long firstCycleTwoRecord = BtsExcelFixture.records().stream() .filter(record -> record.cycleNo() == 2).findFirst().orElseThrow().sequenceId(); assertEquals(firstCycleTwoRecord, simulator.btsPlayback(1, 1).playback().nextSequence()); Instant jumpedAt = simulator.btsPlayback(1, 1).playback().wallClockAt(); simulator.advanceBts(jumpedAt.plusSeconds(60)); assertEquals(2, simulator.btsRecords(1, 1).get(0).cycleNo()); simulator.setBtsSpeed(1, 1, 600); Instant wallClock = simulator.btsPlayback(1, 1).playback().wallClockAt(); long durationMillis = BtsExcelFixture.record(BtsExcelFixture.recordCount()).offsetSeconds() * 1000L; simulator.advanceBts(wallClock.plusMillis(durationMillis / 600L + 1000L)); assertTrue(simulator.btsPlayback(1, 1).playback().naturallyFinished()); simulator.resetBts(1, 1); assertEquals(0, simulator.btsRecords(1, 1).size()); assertEquals(1, simulator.btsPlayback(1, 1).playback().cycleNo()); } try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store)) { assertEquals(0, restored.btsRecords(1, 1).size()); assertEquals(1, restored.btsPlayback(1, 1).playback().cycleNo()); } } @Test void powerCycleResetsBtsFixtureLifecycleAndClearsHistory() throws Exception { InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(); try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store)) { simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING); simulator.nextBtsCycle(1, 1); assertEquals(2, simulator.btsPlayback(1, 1).playback().cycleNo()); simulator.advanceBts(Instant.now().plusSeconds(60)); int historyCount = simulator.btsRecords(1, 1).size(); simulator.setAllDevicesPoweredOn(false); simulator.setAllDevicesPoweredOn(true); BtsDevelopmentSimulator.PlaybackSnapshot playback = simulator.btsPlayback(1, 1); assertEquals(1, playback.playback().cycleNo()); assertEquals(1, playback.playback().stepNo()); assertFalse(playback.playback().naturallyFinished()); assertTrue(historyCount > 0); assertEquals(0, playback.records().size()); } } @Test void playbackFollowsExcelFixtureThroughAllCyclesAndRecords() throws Exception { Instant start = Instant.parse("2026-08-31T00:00:00Z"); try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore())) { simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING); simulator.advanceBts(start); simulator.setBtsSpeed(1, 1, 600); Instant wallClock = simulator.btsPlayback(1, 1).playback().wallClockAt(); long durationMillis = BtsExcelFixture.record(BtsExcelFixture.recordCount()).offsetSeconds() * 1000L; simulator.advanceBts(wallClock.plusMillis(durationMillis / 600L + 1000L)); var records = simulator.btsRecords(1, 1); assertEquals(7605, records.size()); assertEquals(7605L, records.get(records.size() - 1).sequenceId()); assertEquals(33, records.get(records.size() - 1).cycleNo()); assertEquals(33, simulator.btsCycles(1, 1).size()); assertEquals(129, simulator.btsSteps(1, 1).size()); assertTrue(simulator.btsPlayback(1, 1).playback().naturallyFinished()); } } }