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

  alias Microwaveprop.Weather.Grib2.Section

  describe "parse_message/1" do
    test "parses a synthetic GRIB2 message" do
      msg = build_synthetic_grib2()
      assert {:ok, parsed} = Section.parse_message(msg)

      # Grid params from Section 3
      assert parsed.grid_params.nx == 1799
      assert parsed.grid_params.ny == 1059
      assert_in_delta parsed.grid_params.la1, 21.138123, 0.000001
      assert_in_delta parsed.grid_params.lo1, 237.280472, 0.000001
      assert_in_delta parsed.grid_params.dx, 3000.0, 0.1
      assert_in_delta parsed.grid_params.dy, 3000.0, 0.1
      assert_in_delta parsed.grid_params.latin1, 38.5, 0.000001
      assert_in_delta parsed.grid_params.latin2, 38.5, 0.000001
      assert parsed.grid_params.scan_mode == 64

      # Product from Section 4
      assert parsed.product.var == "TMP"
      assert parsed.product.level == "2 m above ground"

      # Packing params from Section 5
      assert parsed.packing_params.num_data_points == 100
      assert parsed.packing_params.bits_per_value == 16

      # Data from Section 7
      assert is_binary(parsed.data)

      # Bitmap indicator
      assert parsed.bitmap == :none
    end

    test "rejects non-GRIB2 binary" do
      assert {:error, _} = Section.parse_message(<<"NOT_GRIB", 0::64-big>>)
    end

    test "rejects GRIB edition 1" do
      indicator = <<"GRIB", 0::16, 0::8, 1::8, 100::64-big>>
      assert {:error, _} = Section.parse_message(indicator)
    end

    test "parses message with trailing padding bytes instead of 7777" do
      msg = build_synthetic_grib2_with_trailing_padding()
      assert {:ok, parsed} = Section.parse_message(msg)
      assert parsed.product.var == "TMP"
      assert parsed.bitmap == :none
    end

    test "parses message with truncated trailing section" do
      msg = build_synthetic_grib2_with_truncated_section()
      assert {:ok, parsed} = Section.parse_message(msg)
      assert parsed.product.var == "TMP"
      assert parsed.bitmap == :none
    end
  end

  describe "product identification" do
    test "identifies all supported variables" do
      # discipline 0 (meteorological)
      assert Section.identify_var(0, 0, 0) == "TMP"
      assert Section.identify_var(0, 0, 6) == "DPT"
      assert Section.identify_var(0, 3, 0) == "PRES"
      assert Section.identify_var(0, 3, 5) == "HGT"
      assert Section.identify_var(0, 3, 18) == "HPBL"
      assert Section.identify_var(0, 1, 3) == "PWAT"
    end

    test "returns category:number for unknown variables" do
      assert Section.identify_var(0, 99, 99) == "99:99"
    end
  end

  describe "level identification" do
    test "surface" do
      assert Section.identify_level(1, 0) == "surface"
    end

    test "pressure level in mb" do
      # Stored in Pa, displayed in mb
      assert Section.identify_level(100, 100_000) == "1000 mb"
      assert Section.identify_level(100, 85_000) == "850 mb"
    end

    test "meters above ground" do
      assert Section.identify_level(103, 2) == "2 m above ground"
      assert Section.identify_level(103, 10) == "10 m above ground"
    end

    test "entire atmosphere" do
      assert Section.identify_level(200, 0) ==
               "entire atmosphere (considered as a single layer)"
    end

    test "unknown level type" do
      assert Section.identify_level(255, 0) == "unknown:255:0"
    end
  end

  describe "sign_magnitude_16/1" do
    test "positive value" do
      assert Section.sign_magnitude_16(<<0::1, 5::15>>) == 5
    end

    test "negative value" do
      assert Section.sign_magnitude_16(<<1::1, 5::15>>) == -5
    end

    test "zero" do
      assert Section.sign_magnitude_16(<<0::16>>) == 0
    end
  end

  # --- Helpers ---

  defp build_synthetic_grib2 do
    # Section 1 (Identification) - minimal
    sec1_body = <<
      # Originating center, subcenter, etc.
      0::16-big,
      0::16-big,
      # Master/local tables
      2::8,
      1::8,
      # Significance of reference time
      1::8,
      # Year, month, day, hour, minute, second
      2026::16-big,
      3::8,
      28::8,
      18::8,
      0::8,
      0::8,
      # Production status, type
      0::8,
      1::8
    >>

    sec1 = section_wrap(1, sec1_body)

    # Section 3 (Grid Definition) - Template 3.30 (Lambert Conformal)
    sec3_body = <<
      # Source of grid definition
      0::8,
      # Number of data points
      1_905_141::32-big,
      # Number of octets for optional list
      0::8,
      # Interpretation
      0::8,
      # Grid Definition Template Number = 30 (Lambert Conformal)
      30::16-big,
      # Shape of earth (6 = spherical, R=6371229)
      6::8,
      # Scale factor/value of radius (not used for shape 6)
      0::8,
      0::32-big,
      # Scale factor/value of major axis
      0::8,
      0::32-big,
      # Scale factor/value of minor axis
      0::8,
      0::32-big,
      # Nx, Ny
      1799::32-big,
      1059::32-big,
      # La1 in microdegrees
      21_138_123::32-big,
      # Lo1 in microdegrees
      237_280_472::32-big,
      # Resolution and component flags
      0b00111000::8,
      # LaD in microdegrees
      38_500_000::32-big,
      # LoV in microdegrees
      262_500_000::32-big,
      # Dx in millimetres
      3_000_000::32-big,
      # Dy in millimetres
      3_000_000::32-big,
      # Projection centre flag
      0::8,
      # Scanning mode
      64::8,
      # Latin1 in microdegrees
      38_500_000::32-big,
      # Latin2 in microdegrees
      38_500_000::32-big,
      # Latitude/longitude of southern pole
      0::32-big,
      0::32-big
    >>

    sec3 = section_wrap(3, sec3_body)

    # Section 4 (Product Definition) - Template 4.0
    sec4_body = <<
      # Number of coordinate values
      0::16-big,
      # Product Definition Template Number = 0
      0::16-big,
      # Parameter category (0 = Temperature)
      0::8,
      # Parameter number (0 = Temperature)
      0::8,
      # Type of generating process
      2::8,
      # Background + forecast generating process
      0::8,
      0::8,
      # Hours/minutes of observational data cutoff
      0::16-big,
      0::8,
      # Indicator of unit of time range (1=hour)
      1::8,
      # Forecast time
      0::32-big,
      # Type of first fixed surface (103 = above ground)
      103::8,
      # Scale factor and scaled value of first fixed surface
      0::8,
      2::32-big,
      # Type of second fixed surface (255 = missing)
      255::8,
      0::8,
      0::32-big
    >>

    sec4 = section_wrap(4, sec4_body)

    # Section 5 (Data Representation) - Template 5.0 (Simple packing)
    # Reference value as IEEE 754 float32
    ref_val = <<0.0::float-32-big>>

    sec5_body = <<
      # Number of data points
      100::32-big,
      # Data Representation Template Number = 0
      0::16-big,
      # Reference value (IEEE 754)
      ref_val::binary,
      # Binary scale factor (sign-magnitude 16-bit)
      0::16-big,
      # Decimal scale factor (sign-magnitude 16-bit)
      0::16-big,
      # Number of bits per value
      16::8
    >>

    sec5 = section_wrap(5, sec5_body)

    # Section 6 (Bitmap) - no bitmap
    sec6_body = <<255::8>>
    sec6 = section_wrap(6, sec6_body)

    # Section 7 (Data) - 100 values of 16 bits = 200 bytes
    data_bytes = :binary.copy(<<42::16-big>>, 100)
    sec7 = section_wrap(7, data_bytes)

    # Section 8 (End)
    sec8 = "7777"

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

    # Section 0 (Indicator)
    sec0 = <<"GRIB", 0::16, 0::8, 2::8, total_length::64-big>>

    sec0 <> body
  end

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

  # Like build_synthetic_grib2 but with 3 padding bytes instead of "7777"
  defp build_synthetic_grib2_with_trailing_padding do
    body = build_sections_body() <> <<0, 0, 0>>
    total_length = 16 + byte_size(body)
    sec0 = <<"GRIB", 0::16, 0::8, 2::8, total_length::64-big>>
    sec0 <> body
  end

  # Like build_synthetic_grib2 but total_length includes extra bytes after "7777"
  # that look like a truncated section header
  defp build_synthetic_grib2_with_truncated_section do
    # All valid sections + "7777" + a truncated next section (length says 500 but only 10 bytes)
    truncated = <<500::32-big, 4::8, 0, 0, 0, 0, 0>>
    body = build_sections_body() <> "7777" <> truncated
    total_length = 16 + byte_size(body)
    sec0 = <<"GRIB", 0::16, 0::8, 2::8, total_length::64-big>>
    sec0 <> body
  end

  defp build_sections_body do
    sec1_body = <<
      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
    >>

    sec3_body = <<
      0::8,
      1_905_141::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,
      1799::32-big,
      1059::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
    >>

    sec4_body = <<
      0::16-big,
      0::16-big,
      0::8,
      0::8,
      2::8,
      0::8,
      0::8,
      0::16-big,
      0::8,
      1::8,
      0::32-big,
      103::8,
      0::8,
      2::32-big,
      255::8,
      0::8,
      0::32-big
    >>

    ref_val = <<0.0::float-32-big>>

    sec5_body = <<
      100::32-big,
      0::16-big,
      ref_val::binary,
      0::16-big,
      0::16-big,
      16::8
    >>

    sec6_body = <<255::8>>
    data_bytes = :binary.copy(<<42::16-big>>, 100)

    section_wrap(1, sec1_body) <>
      section_wrap(3, sec3_body) <>
      section_wrap(4, sec4_body) <>
      section_wrap(5, sec5_body) <>
      section_wrap(6, sec6_body) <>
      section_wrap(7, data_bytes)
  end
end
