package com.ruoyi.device.bts;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import com.ruoyi.device.model.ChannelIdentity;
import com.ruoyi.device.model.DeviceWorkStatus;
import com.ruoyi.device.model.HistoryPage;
import com.ruoyi.device.model.HistoryAvailability;
import com.ruoyi.device.model.RealtimeSnapshot;
class BtsXmlCodecTest
{
private final BtsXmlCodec codec = new BtsXmlCodec("lab-bts", ZoneId.of("Asia/Hong_Kong"));
@Test
void parsesDeviceDiscoveryAndChannelIdentity()
{
String xml = "getdevinfo_resp"
+ "true"
+ "";
var channels = codec.parseDeviceInfo(xml);
assertEquals(1, channels.size());
assertEquals(new ChannelIdentity("lab-bts", 22, 1, 2, 3), channels.get(0).identity());
assertEquals("192.168.1.35", channels.get(0).serverIp());
}
@Test
void rejectsInvalidDiscoveryAvailability()
{
String xml = "getdevinfo_resp"
+ "maybe"
+ "";
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
() -> codec.parseDeviceInfo(xml));
assertEquals(BtsProtocolException.Reason.INVALID_VALUE, exception.reason());
}
@Test
void parsesRealtimeCoreDynamicAndDbcMeasurements()
{
String xml = "inquire_resp"
+ ""
+ ""
+ "
";
Map result = codec.parseSnapshots(xml, Instant.parse("2026-08-25T10:00:00Z"));
RealtimeSnapshot snapshot = result.values().iterator().next();
assertEquals(DeviceWorkStatus.WORKING, snapshot.workStatus());
assertEquals(new BigDecimal("1.2"), snapshot.currentA());
assertEquals(new BigDecimal("23.4"), snapshot.measurements().get("T1").value());
assertEquals("%", snapshot.measurements().get("DBC:Frame1:Soc").unit());
}
@Test
void mapsUnknownWorkStatusWithoutDiscardingSnapshot()
{
String xml = "inquire_resp
";
RealtimeSnapshot snapshot = codec.parseSnapshots(xml, Instant.EPOCH).values().iterator().next();
assertEquals(DeviceWorkStatus.UNKNOWN, snapshot.workStatus());
}
@Test
void mapsFinishAsNormalCompletion()
{
String xml = "inquire_resp"
+ "
";
RealtimeSnapshot snapshot = codec.parseSnapshots(xml, Instant.EPOCH).values().iterator().next();
assertEquals(DeviceWorkStatus.FINISHED, snapshot.workStatus());
}
@Test
void parsesHistoryPageAndHongKongDeviceTime()
{
ChannelIdentity channel = new ChannelIdentity("lab-bts", 24, 1, 1, 1);
String xml = "download_resp"
+ ""
+ ""
+ ""
+ "
";
HistoryPage page = codec.parseHistory(xml, channel, "42", 7);
assertEquals("42", page.btsTestId());
assertEquals(8, page.nextPosition());
assertEquals(Instant.parse("2026-08-25T10:00:00Z"), page.samples().get(0).deviceTime());
}
@Test
void rejectsHistoryResponseForDifferentChannelOrTest()
{
ChannelIdentity requested = new ChannelIdentity("lab-bts", 24, 1, 1, 1);
String xml = "download_resp"
+ ""
+ "
";
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
() -> codec.parseHistory(xml, requested, "42", 1));
assertEquals(BtsProtocolException.Reason.IDENTITY_MISMATCH, exception.reason());
}
@Test
void buildsAndParsesHistoryAvailabilityForMultipleChannels()
{
ChannelIdentity first = new ChannelIdentity("lab-bts", 22, 3, 2, 8);
ChannelIdentity second = new ChannelIdentity("lab-bts", 22, 3, 1, 6);
String request = codec.inquireHistoryRequest(Set.of(first, second));
String response = "inquiredf_resp"
+ "true"
+ "false"
+ "
";
List result = codec.parseHistoryAvailability(response);
assertTrue(request.contains("inquiredf"));
assertTrue(request.contains("devtype=\"22\" devid=\"3\" subdevid=\"2\" chlid=\"8\" testid=\"0\""));
assertTrue(request.contains("devtype=\"22\" devid=\"3\" subdevid=\"1\" chlid=\"6\" testid=\"0\""));
assertEquals(new HistoryAvailability(first, "11", 10, true), result.get(0));
assertEquals(new HistoryAvailability(second, "10", 11, false), result.get(1));
}
@Test
void rejectsIllegalNumericData()
{
String xml = "inquire_resp
";
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
() -> codec.parseSnapshots(xml, Instant.EPOCH));
assertEquals(BtsProtocolException.Reason.INVALID_VALUE, exception.reason());
}
@Test
void classifiesWrongCommandAndMalformedXml()
{
BtsProtocolException wrongCommand = assertThrows(BtsProtocolException.class,
() -> codec.parseSnapshots("download_resp", Instant.EPOCH));
BtsProtocolException malformed = assertThrows(BtsProtocolException.class,
() -> codec.parseSnapshots("inquire_resp", Instant.EPOCH));
assertEquals(BtsProtocolException.Reason.UNEXPECTED_COMMAND, wrongCommand.reason());
assertEquals(BtsProtocolException.Reason.MALFORMED_XML, malformed.reason());
}
@Test
void reportsMissingRequiredField()
{
String xml = "inquire_resp
";
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
() -> codec.parseSnapshots(xml, Instant.EPOCH));
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, exception.reason());
}
@Test
void rejectsResponsesMissingRequiredContainers()
{
BtsProtocolException discovery = assertThrows(BtsProtocolException.class,
() -> codec.parseDeviceInfo("getdevinfo_resp"));
BtsProtocolException realtime = assertThrows(BtsProtocolException.class,
() -> codec.parseSnapshots("inquire_resp", Instant.EPOCH));
BtsProtocolException availability = assertThrows(BtsProtocolException.class,
() -> codec.parseHistoryAvailability("inquiredf_resp"));
ChannelIdentity channel = new ChannelIdentity("lab-bts", 24, 1, 1, 1);
BtsProtocolException history = assertThrows(BtsProtocolException.class,
() -> codec.parseHistory("download_resp"
+ ""
+ "", channel, "42", 1));
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, discovery.reason());
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, realtime.reason());
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, availability.reason());
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, history.reason());
}
@Test
void rejectsDoctypeAndExternalEntities()
{
String xml = "]>inquire_resp&xxe;";
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
() -> codec.parseSnapshots(xml, Instant.EPOCH));
assertFalse(exception.getMessage().isBlank());
}
}