package org.terst.nav.ais import org.junit.Assert.* import org.junit.Test class CpaAlertTest { // --- CpaCalculator --- @Test fun `head-on convergence produces small CPA and positive TCPA`() { // Own vessel heading north at 10 kt; target heading south at 10 kt, 1 nm ahead val (cpa, tcpa) = CpaCalculator.compute( ownLat = 0.0, ownLon = 0.0, ownSog = 10.0, ownCog = 0.0, tgtLat = 1.0 / 60.0, tgtLon = 0.0, tgtSog = 10.0, tgtCog = 180.0 ) assertTrue("CPA should be near zero, got $cpa", cpa < 0.1) assertTrue("TCPA should be positive, got $tcpa", tcpa > 0) assertTrue("TCPA should be < 5 min, got $tcpa", tcpa < 5.0) } @Test fun `diverging vessels produce negative TCPA`() { // Own heading north, target heading north ahead — both going same direction with target faster val (_, tcpa) = CpaCalculator.compute( ownLat = 0.0, ownLon = 0.0, ownSog = 5.0, ownCog = 0.0, tgtLat = 1.0 / 60.0, tgtLon = 0.0, tgtSog = 10.0, tgtCog = 0.0 ) assertTrue("TCPA should be negative for diverging vessels, got $tcpa", tcpa < 0) } @Test fun `zero relative velocity returns current distance`() { // Same velocity — parallel course, same speed val (cpa, tcpa) = CpaCalculator.compute( ownLat = 0.0, ownLon = 0.0, ownSog = 5.0, ownCog = 90.0, tgtLat = 1.0 / 60.0, tgtLon = 0.0, tgtSog = 5.0, tgtCog = 90.0 ) assertTrue("CPA should equal current distance, got $cpa", cpa > 0.9 && cpa < 1.1) assertEquals(0.0, tcpa, 1e-9) } @Test fun `well-separated crossing vessels produce large CPA`() { // Perpendicular courses but far apart — should not alert val (cpa, _) = CpaCalculator.compute( ownLat = 0.0, ownLon = 0.0, ownSog = 8.0, ownCog = 0.0, tgtLat = 0.0, tgtLon = 5.0, tgtSog = 8.0, tgtCog = 270.0 ) assertTrue("CPA should be > 2 nm for well-separated vessels, got $cpa", cpa > 2.0) } // --- Severity classification --- @Test fun `DANGER severity for CPA under 0_5 nm`() { val alert = CpaAlert(123, "TARGET", 0.3, 10.0, CpaSeverity.DANGER) assertEquals(CpaSeverity.DANGER, alert.severity) } @Test fun `label formats correctly with vessel name`() { val alert = CpaAlert(123456789, "STENA IMPERO", 0.8, 12.5, CpaSeverity.WARNING) val lbl = alert.label() assertTrue(lbl.contains("0.8")) assertTrue(lbl.contains("13") || lbl.contains("12")) // rounded min assertTrue(lbl.contains("STENA")) } @Test fun `label uses MMSI prefix when name is blank`() { val alert = CpaAlert(987654321, "", 1.5, 20.0, CpaSeverity.CAUTION) // "MMSI 987654321" is 14 chars; label() takes the first 12 -> "MMSI 9876543" val lbl = alert.label() assertTrue("Expected MMSI prefix in label, got: $lbl", lbl.contains("MMSI")) assertTrue("Expected partial MMSI in label, got: $lbl", lbl.contains("987654")) } }