defmodule Microwaveprop.WeatherGridTest do
  use Microwaveprop.DataCase, async: false

  alias Microwaveprop.Propagation.ProfilesFile
  alias Microwaveprop.Repo
  alias Microwaveprop.Weather
  alias Microwaveprop.Weather.GridCache

  setup do
    # GridCache ETS state leaks between tests (it's a named table outside the
    # test sandbox). Clear it so each test gets a fresh cache and the DB load
    # path actually runs.
    GridCache.clear()
    # Microwaveprop.Cache now memoizes ProfilesFile.read/1 keyed by
    # `(base_dir, valid_time)`. Tests within this file share the default
    # base_dir and frequently reuse `DateTime.utc_now()` truncated to seconds,
    # so a prior test's cached read leaks into the next one's assertion.
    Microwaveprop.Cache.clear()
    :ok
  end

  describe "load_weather_grid/1" do
    test "returns grid data for latest valid_time within bounds" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      insert_hrrr_profile(%{
        lat: 32.125,
        lon: -97.375,
        valid_time: now,
        surface_temp_c: 25.0,
        surface_dewpoint_c: 18.0,
        surface_pressure_mb: 1013.0,
        hpbl_m: 500.0,
        pwat_mm: 30.0,
        min_refractivity_gradient: -280.0,
        ducting_detected: true
      })

      insert_hrrr_profile(%{
        lat: 32.250,
        lon: -97.375,
        valid_time: now,
        surface_temp_c: 26.0,
        surface_dewpoint_c: 19.0,
        surface_pressure_mb: 1012.0,
        hpbl_m: 600.0,
        pwat_mm: 32.0,
        min_refractivity_gradient: -150.0,
        ducting_detected: false
      })

      bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
      result = Weather.load_weather_grid(bounds)

      assert length(result) == 2
      point = Enum.find(result, &(&1.lat == 32.125))
      assert point.temperature == 25.0
      assert point.dewpoint_depression == 7.0
      assert point.bl_height == 500.0
      assert point.pwat == 30.0
      assert point.refractivity_gradient == -280.0
      assert point.ducting == true
    end

    test "returns empty list when no data exists" do
      bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
      assert Weather.load_weather_grid(bounds) == []
    end

    test "only returns points on 0.125 grid" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      insert_hrrr_profile(%{
        lat: 32.125,
        lon: -97.375,
        valid_time: now,
        surface_temp_c: 25.0,
        surface_dewpoint_c: 18.0,
        surface_pressure_mb: 1013.0,
        hpbl_m: 500.0,
        pwat_mm: 30.0
      })

      insert_hrrr_profile(%{
        lat: 32.1337,
        lon: -97.3821,
        valid_time: now,
        surface_temp_c: 25.0,
        surface_dewpoint_c: 18.0,
        surface_pressure_mb: 1013.0,
        hpbl_m: 500.0,
        pwat_mm: 30.0
      })

      bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
      result = Weather.load_weather_grid(bounds)

      assert length(result) == 1
      assert hd(result).lat == 32.125
    end

    test "uses most recent valid_time only" do
      old = DateTime.utc_now() |> DateTime.add(-7200, :second) |> DateTime.truncate(:second)
      new = DateTime.truncate(DateTime.utc_now(), :second)

      insert_hrrr_profile(%{
        lat: 32.125,
        lon: -97.375,
        valid_time: old,
        surface_temp_c: 20.0,
        surface_dewpoint_c: 15.0,
        surface_pressure_mb: 1010.0,
        hpbl_m: 400.0,
        pwat_mm: 25.0
      })

      insert_hrrr_profile(%{
        lat: 32.125,
        lon: -97.375,
        valid_time: new,
        surface_temp_c: 28.0,
        surface_dewpoint_c: 20.0,
        surface_pressure_mb: 1015.0,
        hpbl_m: 700.0,
        pwat_mm: 35.0
      })

      bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
      result = Weather.load_weather_grid(bounds)

      assert length(result) == 1
      assert hd(result).temperature == 28.0
    end

    test "includes derived upper-air fields and strips raw profile data" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      profile = [
        %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0},
        %{"pres" => 925.0, "tmpc" => 20.0, "dwpc" => 14.0, "hght" => 800.0},
        %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0},
        %{"pres" => 700.0, "tmpc" => 5.0, "dwpc" => -2.0, "hght" => 3100.0},
        %{"pres" => 500.0, "tmpc" => -15.0, "dwpc" => -25.0, "hght" => 5600.0}
      ]

      duct_chars = [
        %{"base" => 200.0, "strength" => 50.0},
        %{"base" => 800.0, "strength" => 30.0}
      ]

      insert_hrrr_profile(%{
        lat: 32.125,
        lon: -97.375,
        valid_time: now,
        surface_temp_c: 25.0,
        surface_dewpoint_c: 18.0,
        surface_pressure_mb: 1013.0,
        surface_refractivity: 320.0,
        hpbl_m: 500.0,
        pwat_mm: 30.0,
        min_refractivity_gradient: -280.0,
        ducting_detected: true,
        profile: profile,
        duct_characteristics: duct_chars
      })

      bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
      [point] = Weather.load_weather_grid(bounds)

      # Derived fields should be present
      assert is_float(point.surface_rh)
      assert point.surface_refractivity == 320.0
      assert point.temp_850mb == 15.0
      assert point.dewpoint_850mb == 10.0
      assert is_float(point.lapse_rate)
      assert point.duct_base_m == 200.0
      assert point.duct_strength == 50.0

      # Raw bulky fields should be stripped
      refute Map.has_key?(point, :profile)
      refute Map.has_key?(point, :duct_characteristics)
      refute Map.has_key?(point, :surface_temp_c)
      refute Map.has_key?(point, :surface_dewpoint_c)
    end
  end

  describe "build_grid_cache_rows/2" do
    test "returns an empty list for empty grid_data" do
      now = DateTime.truncate(DateTime.utc_now(), :second)
      assert Weather.build_grid_cache_rows(%{}, now) == []
    end

    test "builds a derived row for each in-memory grid point without hitting the database" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      profile = [
        %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0},
        %{"pres" => 925.0, "tmpc" => 20.0, "dwpc" => 14.0, "hght" => 800.0},
        %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0},
        %{"pres" => 700.0, "tmpc" => 5.0, "dwpc" => -2.0, "hght" => 3100.0},
        %{"pres" => 500.0, "tmpc" => -15.0, "dwpc" => -25.0, "hght" => 5600.0}
      ]

      grid_data = %{
        {32.125, -97.375} => %{
          profile: profile,
          surface_temp_c: 25.0,
          surface_dewpoint_c: 18.0,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0
        },
        {32.250, -97.375} => %{
          profile: profile,
          surface_temp_c: 26.0,
          surface_dewpoint_c: 19.0,
          surface_pressure_mb: 1012.0,
          hpbl_m: 600.0,
          pwat_mm: 32.0
        }
      }

      result = Weather.build_grid_cache_rows(grid_data, now)

      assert length(result) == 2

      point = Enum.find(result, &(&1.lat == 32.125))
      assert point.lon == -97.375
      assert point.valid_time == now
      assert point.temperature == 25.0
      assert point.dewpoint_depression == 7.0
      assert point.bl_height == 500.0
      assert point.pwat == 30.0
      assert point.surface_pressure_mb == 1013.0
    end

    test "computes surface_refractivity and min_refractivity_gradient from the profile" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      profile = [
        %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0},
        %{"pres" => 925.0, "tmpc" => 20.0, "dwpc" => 14.0, "hght" => 800.0},
        %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0},
        %{"pres" => 700.0, "tmpc" => 5.0, "dwpc" => -2.0, "hght" => 3100.0},
        %{"pres" => 500.0, "tmpc" => -15.0, "dwpc" => -25.0, "hght" => 5600.0}
      ]

      grid_data = %{
        {32.125, -97.375} => %{
          profile: profile,
          surface_temp_c: 25.0,
          surface_dewpoint_c: 18.0,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0
        }
      }

      [point] = Weather.build_grid_cache_rows(grid_data, now)

      assert is_float(point.surface_refractivity)
      assert is_float(point.refractivity_gradient)
      assert is_float(point.surface_rh)
      assert point.temp_850mb == 15.0
      assert point.dewpoint_850mb == 10.0
    end

    test "skips points with missing or physically impossible surface temperature" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      profile = [
        %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0},
        %{"pres" => 925.0, "tmpc" => 20.0, "dwpc" => 14.0, "hght" => 800.0},
        %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0}
      ]

      grid_data = %{
        {32.125, -97.375} => %{
          profile: profile,
          surface_temp_c: nil,
          surface_dewpoint_c: nil,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0
        },
        {32.250, -97.375} => %{
          profile: profile,
          surface_temp_c: 250.0,
          surface_dewpoint_c: 200.0,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0
        },
        {32.375, -97.375} => %{
          profile: profile,
          surface_temp_c: 25.0,
          surface_dewpoint_c: 18.0,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0
        }
      }

      result = Weather.build_grid_cache_rows(grid_data, now)

      assert length(result) == 1
      assert hd(result).lat == 32.375
    end

    test "with a bounds filter only derives in-bounds points" do
      # Filter-before-derive: when called with bounds, the builder must
      # skip the `SoundingParams.derive` work for every point outside
      # the viewport. Out-of-bounds points are left as raw profile maps
      # so the heavy per-point derivation cost is bounded by the
      # viewport size, not the full CONUS grid.
      now = DateTime.truncate(DateTime.utc_now(), :second)

      profile = [
        %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0},
        %{"pres" => 925.0, "tmpc" => 20.0, "dwpc" => 14.0, "hght" => 800.0},
        %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0}
      ]

      base_profile = %{
        profile: profile,
        surface_temp_c: 25.0,
        surface_dewpoint_c: 18.0,
        surface_pressure_mb: 1013.0,
        hpbl_m: 500.0,
        pwat_mm: 30.0
      }

      grid_data = %{
        {32.125, -97.375} => base_profile,
        {40.000, -75.000} => base_profile,
        {45.000, -120.000} => base_profile
      }

      bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
      rows = Weather.build_grid_cache_rows(grid_data, now, bounds)

      assert length(rows) == 1
      assert hd(rows).lat == 32.125
      assert hd(rows).lon == -97.375
    end

    test "with nil bounds derives every point (backwards compatible)" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      profile = [
        %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0},
        %{"pres" => 925.0, "tmpc" => 20.0, "dwpc" => 14.0, "hght" => 800.0}
      ]

      grid_data = %{
        {32.125, -97.375} => %{
          profile: profile,
          surface_temp_c: 25.0,
          surface_dewpoint_c: 18.0,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0
        },
        {40.000, -75.000} => %{
          profile: profile,
          surface_temp_c: 22.0,
          surface_dewpoint_c: 15.0,
          surface_pressure_mb: 1015.0,
          hpbl_m: 400.0,
          pwat_mm: 25.0
        }
      }

      assert length(Weather.build_grid_cache_rows(grid_data, now, nil)) == 2
    end

    test "strips raw profile/duct_characteristics/surface_temp_c/surface_dewpoint_c fields" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      profile = [
        %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0},
        %{"pres" => 925.0, "tmpc" => 20.0, "dwpc" => 14.0, "hght" => 800.0},
        %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0}
      ]

      grid_data = %{
        {32.125, -97.375} => %{
          profile: profile,
          surface_temp_c: 25.0,
          surface_dewpoint_c: 18.0,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0
        }
      }

      [point] = Weather.build_grid_cache_rows(grid_data, now)

      refute Map.has_key?(point, :profile)
      refute Map.has_key?(point, :duct_characteristics)
      refute Map.has_key?(point, :surface_temp_c)
      refute Map.has_key?(point, :surface_dewpoint_c)
    end
  end

  describe "weather_point_detail/3" do
    test "returns all fields for a single grid point" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      insert_hrrr_profile(%{
        lat: 32.125,
        lon: -97.375,
        valid_time: now,
        surface_temp_c: 25.0,
        surface_dewpoint_c: 18.0,
        surface_pressure_mb: 1013.0,
        hpbl_m: 500.0,
        pwat_mm: 30.0,
        min_refractivity_gradient: -280.0,
        ducting_detected: true
      })

      result = Weather.weather_point_detail(32.15, -97.40, now)

      assert result.lat == 32.125
      assert result.temperature == 25.0
      assert result.surface_pressure_mb == 1013.0
    end

    test "returns nil when no data at point" do
      now = DateTime.truncate(DateTime.utc_now(), :second)
      assert Weather.weather_point_detail(32.15, -97.40, now) == nil
    end

    test "falls back to ProfilesFile when GridCache is cold and DB has no grid points" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      grid_data = %{
        {32.125, -97.375} => %{
          surface_temp_c: 25.0,
          surface_dewpoint_c: 18.0,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0,
          profile: [
            %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0},
            %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0},
            %{"pres" => 500.0, "tmpc" => -15.0, "dwpc" => -25.0, "hght" => 5600.0}
          ]
        }
      }

      ProfilesFile.write!(now, grid_data)

      result = Weather.weather_point_detail(32.15, -97.40, now)

      assert result.lat == 32.125
      assert result.temperature == 25.0
      assert result.surface_pressure_mb == 1013.0
    end

    test "includes derived upper-air fields and strips raw profile data" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      profile = [
        %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0},
        %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0},
        %{"pres" => 500.0, "tmpc" => -15.0, "dwpc" => -25.0, "hght" => 5600.0}
      ]

      duct_chars = [%{"base" => 300.0, "strength" => 40.0}]

      insert_hrrr_profile(%{
        lat: 32.125,
        lon: -97.375,
        valid_time: now,
        surface_temp_c: 25.0,
        surface_dewpoint_c: 18.0,
        surface_pressure_mb: 1013.0,
        surface_refractivity: 320.0,
        hpbl_m: 500.0,
        pwat_mm: 30.0,
        min_refractivity_gradient: -280.0,
        ducting_detected: true,
        profile: profile,
        duct_characteristics: duct_chars
      })

      result = Weather.weather_point_detail(32.15, -97.40, now)

      # Derived fields
      assert is_float(result.surface_rh)
      assert result.surface_refractivity == 320.0
      assert result.temp_850mb == 15.0
      assert result.duct_base_m == 300.0
      assert result.duct_strength == 40.0

      # Raw fields stripped
      refute Map.has_key?(result, :profile)
      refute Map.has_key?(result, :duct_characteristics)
      refute Map.has_key?(result, :surface_temp_c)
      refute Map.has_key?(result, :surface_dewpoint_c)
    end
  end

  describe "latest_grid_valid_time/0 + ProfilesFile fallback" do
    test "uses ProfilesFile.latest_valid_time when GridCache and DB are both empty" do
      vt = ~U[2026-04-14 18:00:00Z]

      ProfilesFile.write!(vt, %{
        {32.125, -97.375} => %{surface_temp_c: 20.0, surface_dewpoint_c: 15.0}
      })

      assert Weather.latest_grid_valid_time() == vt
    end
  end

  describe "load_weather_grid/1 + ProfilesFile fallback" do
    test "returns derived rows from ProfilesFile when GridCache is empty" do
      vt = ~U[2026-04-14 18:00:00Z]

      ProfilesFile.write!(vt, %{
        {32.125, -97.375} => %{
          surface_temp_c: 25.0,
          surface_dewpoint_c: 18.0,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0
        },
        {32.250, -97.375} => %{
          surface_temp_c: 26.0,
          surface_dewpoint_c: 19.0,
          surface_pressure_mb: 1012.0,
          hpbl_m: 600.0,
          pwat_mm: 32.0
        }
      })

      bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
      rows = Weather.load_weather_grid(bounds)

      assert length(rows) == 2
      assert Enum.any?(rows, &(&1.temperature == 25.0))
      assert Enum.any?(rows, &(&1.temperature == 26.0))
    end
  end

  defp insert_hrrr_profile(attrs) do
    now = DateTime.truncate(DateTime.utc_now(), :second)

    is_gp =
      rem(round(attrs.lat * 1000), 125) == 0 and
        rem(round(attrs.lon * 1000), 125) == 0

    defaults = %{
      id: Ecto.UUID.bingenerate(),
      run_time: DateTime.add(attrs.valid_time, -3600, :second),
      profile: [],
      surface_refractivity: nil,
      min_refractivity_gradient: nil,
      ducting_detected: nil,
      duct_characteristics: nil,
      is_grid_point: is_gp,
      inserted_at: now,
      updated_at: now
    }

    entry = Map.merge(defaults, attrs)
    Repo.insert_all("hrrr_profiles", [entry])
  end
end
