defmodule Microwaveprop.Weather.NexradClientTest do use ExUnit.Case, async: true alias Microwaveprop.Weather.NexradClient describe "round_to_5min/1" do test "rounds down to nearest 5-minute boundary" do dt = ~U[2022-08-20 14:07:32Z] assert NexradClient.round_to_5min(dt) == ~U[2022-08-20 14:05:00Z] end test "already on a 5-minute boundary stays unchanged" do dt = ~U[2022-08-20 14:10:00Z] assert NexradClient.round_to_5min(dt) == ~U[2022-08-20 14:10:00Z] end test "59 minutes rounds down to 55" do dt = ~U[2022-08-20 14:59:59Z] assert NexradClient.round_to_5min(dt) == ~U[2022-08-20 14:55:00Z] end end describe "frame_url/1" do test "builds correct IEM n0q URL from datetime" do dt = ~U[2022-08-20 14:05:00Z] url = NexradClient.frame_url(dt) assert url == "https://mesonet.agron.iastate.edu/archive/data/2022/08/20/GIS/uscomp/n0q_202208201405.png" end test "pads single-digit month and day" do dt = ~U[2022-01-05 03:00:00Z] url = NexradClient.frame_url(dt) assert url == "https://mesonet.agron.iastate.edu/archive/data/2022/01/05/GIS/uscomp/n0q_202201050300.png" end end describe "latlon_to_pixel/2" do test "converts coordinates to pixel positions" do # Top-left corner: lat=50, lon=-126 {x, y} = NexradClient.latlon_to_pixel(50.0, -126.0) assert x == 0 assert y == 0 end test "bottom-right corner" do # lon = -65 -> x = (-65 - -126) / 0.005 = 61 / 0.005 = 12200 # lat = 23 -> y = (50 - 23) / 0.005 = 27 / 0.005 = 5400 {x, y} = NexradClient.latlon_to_pixel(23.0, -65.0) assert x == 12_200 assert y == 5400 end test "DFW area" do # DFW: lat ~32.9, lon ~-97.0 {x, y} = NexradClient.latlon_to_pixel(32.9, -97.0) # x = (-97 - -126) / 0.005 = 29 / 0.005 = 5800 assert x == 5800 # y = (50 - 32.9) / 0.005 = 17.1 / 0.005 = 3420 assert y == 3420 end end describe "extract_box/4" do test "extracts stats from a box of pixel values" do # Create a simple 100x100 image (all zeros except a small region) width = 100 height = 100 pixels = :binary.copy(<<0>>, width * height) # Put some non-zero values around pixel (50, 50) - a 5x5 block pixels = Enum.reduce(48..52, pixels, fn y, acc -> Enum.reduce(48..52, acc, fn x, inner_acc -> offset = y * width + x <> = inner_acc <> end) end) result = NexradClient.extract_box(pixels, width, 50, 50, 5) assert result.pixel_count == 25 assert result.max_reflectivity_dbz > 0 assert result.mean_reflectivity_dbz > 0 assert result.texture_variance == 0.0 end test "returns zeros for an empty box" do width = 100 height = 100 pixels = :binary.copy(<<0>>, width * height) result = NexradClient.extract_box(pixels, width, 50, 50, 5) assert result.pixel_count == 0 assert result.mean_reflectivity_dbz == 0.0 assert result.max_reflectivity_dbz == 0.0 assert result.texture_variance == 0.0 end test "computes texture variance for mixed values" do width = 100 height = 100 pixels = :binary.copy(<<0>>, width * height) # Place alternating values: half 100, half 200 pixels = Enum.reduce(48..52, pixels, fn y, acc -> Enum.reduce(48..52, acc, fn x, inner_acc -> offset = y * width + x value = if rem(x + y, 2) == 0, do: 100, else: 200 <> = inner_acc <> end) end) result = NexradClient.extract_box(pixels, width, 50, 50, 5) assert result.pixel_count == 25 # Variance should be non-zero since we have mixed values assert result.texture_variance > 0.0 end test "handles box at image boundary by clamping" do width = 100 height = 100 pixels = :binary.copy(<<50>>, width * height) # Corner case: near edge result = NexradClient.extract_box(pixels, width, 2, 2, 25) # Should not crash; pixel_count depends on how many pixels are in bounds assert result.pixel_count > 0 end end describe "pixel_to_dbz/1" do test "value 0 maps to 0 (no echo)" do assert NexradClient.pixel_to_dbz(0) == 0.0 end test "value 255 maps to approximately 95 dBZ" do dbz = NexradClient.pixel_to_dbz(255) assert_in_delta dbz, 95.0, 1.0 end test "mid-range value produces reasonable dBZ" do dbz = NexradClient.pixel_to_dbz(128) assert dbz > 0.0 assert dbz < 95.0 end end end