defmodule Microwaveprop.Weather.Grib2.ExtractorTest do
  use ExUnit.Case, async: true

  alias Microwaveprop.Weather.Grib2.Extractor

  # HRRR grid parameters for building synthetic messages
  @hrrr_nx 1799
  @hrrr_ny 1059

  describe "split_messages/1" do
    test "splits concatenated GRIB2 messages" do
      msg1 = build_synthetic_grib2_message(0, 0, 0, 103, 2)
      msg2 = build_synthetic_grib2_message(0, 0, 6, 103, 2)
      blob = msg1 <> msg2

      messages = Extractor.split_messages(blob)
      assert length(messages) == 2
      assert Enum.at(messages, 0) == msg1
      assert Enum.at(messages, 1) == msg2
    end

    test "handles single message" do
      msg = build_synthetic_grib2_message(0, 0, 0, 103, 2)
      messages = Extractor.split_messages(msg)
      assert length(messages) == 1
    end

    test "handles empty binary" do
      assert Extractor.split_messages(<<>>) == []
    end
  end

  describe "extract_points/3" do
    test "extracts single message at first grid point" do
      msg = build_synthetic_grib2_message(0, 0, 0, 103, 2)

      # la1/lo1 = first grid point, so index (0,0) → linear index 0
      assert {:ok, result} = Extractor.extract_points(msg, 21.138123, 237.280472)

      assert Map.has_key?(result, "TMP:2 m above ground")
      assert is_float(result["TMP:2 m above ground"])
    end

    test "extracts multiple concatenated messages" do
      msg1 = build_synthetic_grib2_message(0, 0, 0, 103, 2)
      msg2 = build_synthetic_grib2_message(0, 0, 6, 103, 2)
      msg3 = build_synthetic_grib2_message(0, 3, 0, 1, 0)
      blob = msg1 <> msg2 <> msg3

      assert {:ok, result} = Extractor.extract_points(blob, 21.138123, 237.280472)

      assert Map.has_key?(result, "TMP:2 m above ground")
      assert Map.has_key?(result, "DPT:2 m above ground")
      assert Map.has_key?(result, "PRES:surface")
      assert map_size(result) == 3
    end

    test "handles negative longitude" do
      msg = build_synthetic_grib2_message(0, 0, 0, 103, 2)

      # -122.719528 == 237.280472
      assert {:ok, result} = Extractor.extract_points(msg, 21.138123, -122.719528)
      assert Map.has_key?(result, "TMP:2 m above ground")
    end

    test "returns error for point outside grid" do
      msg = build_synthetic_grib2_message(0, 0, 0, 103, 2)

      # London — way outside HRRR domain
      assert {:error, _} = Extractor.extract_points(msg, 51.5, -0.12)
    end
  end

  describe "linear_index/3" do
    test "scan mode 64 (j positive, i positive, i consecutive)" do
      nx = 100
      scan_mode = 64
      assert Extractor.linear_index({5, 10}, nx, scan_mode) == 10 * 100 + 5
    end

    test "scan mode 0 (j negative, i positive, i consecutive)" do
      # j decreasing means row 0 is top, so index = (ny - 1 - j) * nx + i
      # But we just return j * nx + i for now since we don't flip
      nx = 100
      scan_mode = 0
      assert Extractor.linear_index({5, 10}, nx, scan_mode) == 10 * 100 + 5
    end
  end

  describe "extract_grid/2" do
    test "extracts values for multiple points from a single message" do
      msg = build_synthetic_grib2_message(0, 0, 0, 103, 2)

      # la1/lo1 is the first grid point at (21.138123, 237.280472)
      # Use the same point twice to verify both get values
      points = [{21.138123, -122.719528}, {21.138123, -122.719528}]

      assert {:ok, result} = Extractor.extract_grid(msg, points)

      # Both points are the same, so we should get 1 unique key
      assert map_size(result) == 1
      assert Map.has_key?(result, {21.138123, -122.719528})
      assert Map.has_key?(result[{21.138123, -122.719528}], "TMP:2 m above ground")
    end

    test "extracts from concatenated messages for multiple points" do
      msg1 = build_synthetic_grib2_message(0, 0, 0, 103, 2)
      msg2 = build_synthetic_grib2_message(0, 0, 6, 103, 2)
      blob = msg1 <> msg2

      points = [{21.138123, -122.719528}]

      assert {:ok, result} = Extractor.extract_grid(blob, points)
      point_data = result[{21.138123, -122.719528}]

      assert Map.has_key?(point_data, "TMP:2 m above ground")
      assert Map.has_key?(point_data, "DPT:2 m above ground")
    end

    test "skips points outside the grid" do
      msg = build_synthetic_grib2_message(0, 0, 0, 103, 2)

      # Mix valid and invalid points
      points = [
        {21.138123, -122.719528},
        {51.5, -0.12}
      ]

      assert {:ok, result} = Extractor.extract_grid(msg, points)

      # Only the valid point should be in the result
      assert Map.has_key?(result, {21.138123, -122.719528})
      refute Map.has_key?(result, {51.5, -0.12})
    end

    test "returns empty map for empty points list" do
      msg = build_synthetic_grib2_message(0, 0, 0, 103, 2)

      assert {:ok, result} = Extractor.extract_grid(msg, [])
      assert result == %{}
    end

    test "handles all points outside grid" do
      msg = build_synthetic_grib2_message(0, 0, 0, 103, 2)

      points = [{51.5, -0.12}, {48.8, 2.3}]

      assert {:ok, result} = Extractor.extract_grid(msg, points)
      assert result == %{}
    end
  end

  describe "extract_grid/2 with real HRRR fixture" do
    @fixture_path "test/fixtures/grib2/hrrr_tmp_2m.grib2"
    @multi_fixture_path "test/fixtures/grib2/hrrr_multi.grib2"

    @tag :slow
    @tag timeout: 300_000
    test "extracts grid values matching extract_points for same location" do
      if File.exists?(@fixture_path) do
        fixture = File.read!(@fixture_path)

        lat = 32.78
        lon = -96.8

        {:ok, single_result} = Extractor.extract_points(fixture, lat, lon)
        {:ok, grid_result} = Extractor.extract_grid(fixture, [{lat, lon}])

        point_data = grid_result[{lat, lon}]

        for {key, value} <- single_result do
          assert_in_delta point_data[key], value, 0.001, "#{key}: grid #{point_data[key]} != single #{value}"
        end
      end
    end

    @tag :slow
    @tag timeout: 300_000
    test "extracts grid values for multiple US cities" do
      if File.exists?(@fixture_path) do
        fixture = File.read!(@fixture_path)

        points = [
          {32.78, -96.8},
          {40.71, -74.01},
          {34.05, -118.24}
        ]

        {:ok, result} = Extractor.extract_grid(fixture, points)

        for point <- points do
          assert Map.has_key?(result, point), "Missing point #{inspect(point)}"
          assert Map.has_key?(result[point], "TMP:2 m above ground")

          temp_k = result[point]["TMP:2 m above ground"]
          temp_c = temp_k - 273.15

          assert temp_c > -30.0 and temp_c < 50.0,
                 "#{inspect(point)} temp #{temp_c}C unreasonable"
        end
      end
    end
  end

  describe "extract_points/3 with real HRRR fixture" do
    @fixture_path "test/fixtures/grib2/hrrr_tmp_2m.grib2"
    @multi_fixture_path "test/fixtures/grib2/hrrr_multi.grib2"

    @tag :slow
    @tag timeout: 120_000
    test "extracts TMP:2m from real single-message GRIB2" do
      if File.exists?(@fixture_path) do
        fixture = File.read!(@fixture_path)

        assert {:ok, result} = Extractor.extract_points(fixture, 32.78, -96.8)
        assert Map.has_key?(result, "TMP:2 m above ground")

        temp_k = result["TMP:2 m above ground"]
        temp_c = temp_k - 273.15
        assert temp_c > -10.0 and temp_c < 45.0, "Dallas temp #{temp_c}C unreasonable"
      end
    end

    @tag :slow
    @tag timeout: 300_000
    test "extracts from multi-message GRIB2" do
      if File.exists?(@multi_fixture_path) do
        fixture = File.read!(@multi_fixture_path)

        assert {:ok, result} = Extractor.extract_points(fixture, 32.78, -96.8)
        assert Map.has_key?(result, "TMP:2 m above ground")
        assert Map.has_key?(result, "DPT:2 m above ground")
        assert map_size(result) == 2
      end
    end

    @tag :slow
    @tag timeout: 300_000
    test "DPT values are meteorologically reasonable (not corrupted by bit padding)" do
      if File.exists?(@multi_fixture_path) do
        fixture = File.read!(@multi_fixture_path)

        assert {:ok, result} = Extractor.extract_points(fixture, 32.78, -96.8)

        tmp_k = result["TMP:2 m above ground"]
        dpt_k = result["DPT:2 m above ground"]

        assert tmp_k > 200.0 and tmp_k < 340.0,
               "TMP #{tmp_k}K unreasonable"

        assert dpt_k > 200.0 and dpt_k < 340.0,
               "DPT #{dpt_k}K unreasonable — likely bit-padding corruption"
      end
    end
  end

  # --- Synthetic GRIB2 builder ---

  defp build_synthetic_grib2_message(discipline, param_category, param_number, surface_type, surface_value) do
    num_points = @hrrr_nx * @hrrr_ny

    sec1 = section_wrap(1, build_section1())

    sec3 =
      section_wrap(3, build_section3(@hrrr_nx, @hrrr_ny))

    sec4 =
      section_wrap(
        4,
        build_section4(param_category, param_number, surface_type, surface_value)
      )

    sec5 = section_wrap(5, build_section5(num_points, 16))

    sec6 = section_wrap(6, <<255::8>>)

    # Data: num_points * 2 bytes (16-bit values), all set to 42
    data_bytes = :binary.copy(<<42::16-big>>, num_points)
    sec7 = section_wrap(7, data_bytes)

    sec8 = "7777"

    body = sec1 <> sec3 <> sec4 <> sec5 <> sec6 <> sec7 <> sec8
    total_length = 16 + byte_size(body)

    sec0 = <<"GRIB", 0::16, discipline::8, 2::8, total_length::64-big>>
    sec0 <> body
  end

  defp build_section1 do
    <<
      0::16-big,
      0::16-big,
      2::8,
      1::8,
      1::8,
      2026::16-big,
      3::8,
      28::8,
      18::8,
      0::8,
      0::8,
      0::8,
      1::8
    >>
  end

  defp build_section3(nx, ny) do
    <<
      0::8,
      nx * ny::32-big,
      0::8,
      0::8,
      30::16-big,
      6::8,
      0::8,
      0::32-big,
      0::8,
      0::32-big,
      0::8,
      0::32-big,
      nx::32-big,
      ny::32-big,
      21_138_123::32-big,
      237_280_472::32-big,
      0b00111000::8,
      38_500_000::32-big,
      262_500_000::32-big,
      3_000_000::32-big,
      3_000_000::32-big,
      0::8,
      64::8,
      38_500_000::32-big,
      38_500_000::32-big,
      0::32-big,
      0::32-big
    >>
  end

  defp build_section4(param_category, param_number, surface_type, surface_value) do
    <<
      0::16-big,
      0::16-big,
      param_category::8,
      param_number::8,
      2::8,
      0::8,
      0::8,
      0::16-big,
      0::8,
      1::8,
      0::32-big,
      surface_type::8,
      0::8,
      surface_value::32-big,
      255::8,
      0::8,
      0::32-big
    >>
  end

  defp build_section5(num_points, bits_per_value) do
    <<
      num_points::32-big,
      0::16-big,
      0.0::float-32-big,
      0::16-big,
      0::16-big,
      bits_per_value::8
    >>
  end

  defp section_wrap(section_number, body) do
    length = 5 + byte_size(body)
    <<length::32-big, section_number::8, body::binary>>
  end
end
