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

  alias Microwaveprop.Weather.Grib2.SimplePacking

  describe "extract_value/3" do
    test "basic 16-bit unpacking" do
      # value = (R + X * 2^E) * 10^(-D)
      # R=0.0, E=0, D=0, N=16 → value = X
      params = %{
        reference_value: 0.0,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 16,
        num_data_points: 4
      }

      # 4 values: 100, 200, 300, 400 as 16-bit big-endian
      data = <<100::16-big, 200::16-big, 300::16-big, 400::16-big>>

      assert {:ok, 100.0} = SimplePacking.extract_value(params, data, 0)
      assert {:ok, 200.0} = SimplePacking.extract_value(params, data, 1)
      assert {:ok, 300.0} = SimplePacking.extract_value(params, data, 2)
      assert {:ok, 400.0} = SimplePacking.extract_value(params, data, 3)
    end

    test "applies reference value" do
      # R=273.15, E=0, D=0 → value = 273.15 + X
      params = %{
        reference_value: 273.15,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 16,
        num_data_points: 2
      }

      data = <<10::16-big, 20::16-big>>

      assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
      assert_in_delta val, 283.15, 0.001
    end

    test "applies binary scale factor" do
      # R=0, E=2, D=0 → value = X * 2^2 = X * 4
      params = %{
        reference_value: 0.0,
        binary_scale: 2,
        decimal_scale: 0,
        bits_per_value: 16,
        num_data_points: 1
      }

      data = <<25::16-big>>

      assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
      assert_in_delta val, 100.0, 0.001
    end

    test "applies decimal scale factor" do
      # R=0, E=0, D=2 → value = X * 10^(-2) = X / 100
      params = %{
        reference_value: 0.0,
        binary_scale: 0,
        decimal_scale: 2,
        bits_per_value: 16,
        num_data_points: 1
      }

      data = <<15_000::16-big>>

      assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
      assert_in_delta val, 150.0, 0.001
    end

    test "combined R, E, D" do
      # value = (R + X * 2^E) * 10^(-D)
      # R=100.0, E=1, D=1 → (100 + X * 2) / 10
      params = %{
        reference_value: 100.0,
        binary_scale: 1,
        decimal_scale: 1,
        bits_per_value: 16,
        num_data_points: 1
      }

      data = <<50::16-big>>

      assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
      # (100 + 50 * 2) / 10 = 200 / 10 = 20.0
      assert_in_delta val, 20.0, 0.001
    end

    test "12-bit non-byte-aligned extraction" do
      # 3 values of 12 bits each = 36 bits = 4.5 bytes
      # Values: 0xABC=2748, 0x123=291, 0xDEF=3567
      params = %{
        reference_value: 0.0,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 12,
        num_data_points: 3
      }

      # Pack 3 12-bit values: 0xABC, 0x123, 0xDEF
      # Binary: 1010_1011_1100 | 0001_0010_0011 | 1101_1110_1111
      # As bytes: AB C1 23 DE F0 (with 4 bits padding)
      data = <<0xAB, 0xC1, 0x23, 0xDE, 0xF0>>

      assert {:ok, val0} = SimplePacking.extract_value(params, data, 0)
      assert_in_delta val0, 2748.0, 0.001

      assert {:ok, val1} = SimplePacking.extract_value(params, data, 1)
      assert_in_delta val1, 291.0, 0.001

      assert {:ok, val2} = SimplePacking.extract_value(params, data, 2)
      assert_in_delta val2, 3567.0, 0.001
    end

    test "zero bits_per_value returns reference value" do
      params = %{
        reference_value: 293.5,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 0,
        num_data_points: 10
      }

      # With 0 bits, data is empty
      data = <<>>

      assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
      assert_in_delta val, 293.5, 0.001

      assert {:ok, val} = SimplePacking.extract_value(params, data, 5)
      assert_in_delta val, 293.5, 0.001
    end

    test "negative binary scale factor" do
      # E=-1 → 2^(-1) = 0.5
      params = %{
        reference_value: 0.0,
        binary_scale: -1,
        decimal_scale: 0,
        bits_per_value: 16,
        num_data_points: 1
      }

      data = <<200::16-big>>

      assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
      assert_in_delta val, 100.0, 0.001
    end

    test "index out of range returns error" do
      params = %{
        reference_value: 0.0,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 16,
        num_data_points: 2
      }

      data = <<100::16-big, 200::16-big>>

      assert {:error, :index_out_of_range} = SimplePacking.extract_value(params, data, 5)
    end
  end

  describe "extract_values/3" do
    test "extracts multiple values in a single batch call" do
      params = %{
        reference_value: 0.0,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 16,
        num_data_points: 4
      }

      data = <<100::16-big, 200::16-big, 300::16-big, 400::16-big>>

      assert {:ok, result} = SimplePacking.extract_values(params, data, [0, 2, 3])
      assert map_size(result) == 3
      assert_in_delta result[0], 100.0, 0.001
      assert_in_delta result[2], 300.0, 0.001
      assert_in_delta result[3], 400.0, 0.001
    end

    test "skips out-of-range indices" do
      params = %{
        reference_value: 0.0,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 16,
        num_data_points: 2
      }

      data = <<100::16-big, 200::16-big>>

      assert {:ok, result} = SimplePacking.extract_values(params, data, [0, 5, -1, 1])
      assert map_size(result) == 2
      assert_in_delta result[0], 100.0, 0.001
      assert_in_delta result[1], 200.0, 0.001
    end

    test "returns all same value when bits_per_value is 0" do
      params = %{
        reference_value: 293.5,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 0,
        num_data_points: 10
      }

      assert {:ok, result} = SimplePacking.extract_values(params, <<>>, [0, 3, 7])
      assert map_size(result) == 3
      assert_in_delta result[0], 293.5, 0.001
      assert_in_delta result[3], 293.5, 0.001
      assert_in_delta result[7], 293.5, 0.001
    end

    test "applies scaling correctly to batch" do
      # value = (R + X * 2^E) * 10^(-D)
      # R=100.0, E=1, D=1 -> (100 + X*2) / 10
      params = %{
        reference_value: 100.0,
        binary_scale: 1,
        decimal_scale: 1,
        bits_per_value: 16,
        num_data_points: 2
      }

      data = <<50::16-big, 75::16-big>>

      assert {:ok, result} = SimplePacking.extract_values(params, data, [0, 1])
      # (100 + 50*2)/10 = 20.0
      assert_in_delta result[0], 20.0, 0.001
      # (100 + 75*2)/10 = 25.0
      assert_in_delta result[1], 25.0, 0.001
    end

    test "handles empty indices list" do
      params = %{
        reference_value: 0.0,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 16,
        num_data_points: 2
      }

      data = <<100::16-big, 200::16-big>>

      assert {:ok, result} = SimplePacking.extract_values(params, data, [])
      assert result == %{}
    end

    test "handles non-byte-aligned 12-bit batch extraction" do
      params = %{
        reference_value: 0.0,
        binary_scale: 0,
        decimal_scale: 0,
        bits_per_value: 12,
        num_data_points: 3
      }

      # Pack 3 12-bit values: 0xABC=2748, 0x123=291, 0xDEF=3567
      data = <<0xAB, 0xC1, 0x23, 0xDE, 0xF0>>

      assert {:ok, result} = SimplePacking.extract_values(params, data, [0, 1, 2])
      assert_in_delta result[0], 2748.0, 0.001
      assert_in_delta result[1], 291.0, 0.001
      assert_in_delta result[2], 3567.0, 0.001
    end
  end
end
