defmodule Mix.Tasks.GenVendorTests do
  @shortdoc "Generate test files for vendor modules"

  @moduledoc """
  Generates test files for vendor modules that don't have tests.

  ## Usage

      mix gen_vendor_tests [--dry-run]

  ## Options

    * `--dry-run` - Show what would be generated without creating files
  """

  use Mix.Task

  @vendors_dir "lib/towerops/snmp/profiles/vendors"
  @tests_dir "test/towerops/snmp/profiles/vendors"

  @impl Mix.Task
  def run(args) do
    {opts, _, _} = OptionParser.parse(args, switches: [dry_run: :boolean])
    dry_run = Keyword.get(opts, :dry_run, false)

    # Get all vendor modules (excluding registry and vendor behaviour)
    vendor_files =
      "#{@vendors_dir}/*.ex"
      |> Path.wildcard()
      |> Enum.map(&Path.basename(&1, ".ex"))
      |> Enum.reject(&(&1 in ["registry", "vendor"]))

    # Get existing tests
    existing_tests =
      "#{@tests_dir}/*_test.exs"
      |> Path.wildcard()
      |> Enum.map(&Path.basename(&1, "_test.exs"))

    # Find vendors without tests
    vendors_without_tests =
      vendor_files
      |> Enum.reject(&(&1 in existing_tests))
      |> Enum.sort()

    IO.puts("Found #{length(vendor_files)} vendor modules")
    IO.puts("Found #{length(existing_tests)} existing tests")
    IO.puts("#{length(vendors_without_tests)} vendors need tests")

    if vendors_without_tests == [] do
      IO.puts("\nAll vendor modules have tests!")
      :ok
    else
      Enum.each(vendors_without_tests, fn vendor_file ->
        generate_vendor_test(vendor_file, dry_run)
      end)

      if !dry_run do
        IO.puts("\n\nGenerated #{length(vendors_without_tests)} test files.")
        IO.puts("Run `mix test test/towerops/snmp/profiles/vendors/` to verify.")
      end
    end
  end

  defp generate_vendor_test(vendor_file, dry_run) do
    module_name = Macro.camelize(vendor_file)

    # Read the vendor module to get profile names
    vendor_path = "#{@vendors_dir}/#{vendor_file}.ex"
    profile_names = extract_profile_names(vendor_path)

    content = generate_test_content(module_name, profile_names)

    if dry_run do
      IO.puts("Would generate: #{@tests_dir}/#{vendor_file}_test.exs")
    else
      path = "#{@tests_dir}/#{vendor_file}_test.exs"
      File.write!(path, content)
      IO.puts("Generated: #{path}")
    end
  end

  defp extract_profile_names(path) do
    case File.read(path) do
      {:ok, content} ->
        case Regex.run(~r/def profile_names.*?do:?\s*(\[.*?\])/s, content) do
          [_, list_str] ->
            # Parse the list from the string
            {result, _} = Code.eval_string(list_str)
            result

          _ ->
            []
        end

      _ ->
        []
    end
  end

  defp generate_test_content(module_name, profile_names) do
    profiles_inspect = inspect(profile_names)

    """
    defmodule Towerops.Snmp.Profiles.Vendors.#{module_name}Test do
      use Towerops.DataCase, async: true

      import Mox

      alias Towerops.Snmp.Profiles.Vendors.#{module_name}
      alias Towerops.Snmp.SnmpMock

      setup :verify_on_exit!

      @client_opts [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161,
        timeout: 5000
      ]

      describe "profile_names/0" do
        test "returns expected profile names" do
          assert #{module_name}.profile_names() == #{profiles_inspect}
        end
      end

      describe "detect_hardware/1" do
        test "returns hardware description when SNMP responds" do
          expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.1.0", _ ->
            {:ok, "Test Device Model XYZ"}
          end)

          assert #{module_name}.detect_hardware(@client_opts) == "Test Device Model XYZ"
        end

        test "returns nil when SNMP fails" do
          expect(SnmpMock, :get, fn _, _, _ ->
            {:error, :timeout}
          end)

          assert #{module_name}.detect_hardware(@client_opts) == nil
        end

        test "truncates long descriptions to 64 characters" do
          long_descr = String.duplicate("A", 100)

          expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.1.0", _ ->
            {:ok, long_descr}
          end)

          result = #{module_name}.detect_hardware(@client_opts)
          assert String.length(result) == 64
        end
      end

      describe "wireless_oid_defs/0" do
        test "returns list of sensor definitions" do
          defs = #{module_name}.wireless_oid_defs()

          assert is_list(defs)
          assert length(defs) >= 1
        end

        test "each definition has required fields" do
          defs = #{module_name}.wireless_oid_defs()

          Enum.each(defs, fn def ->
            assert Map.has_key?(def, :oid)
            assert Map.has_key?(def, :sensor_descr)
            assert Map.has_key?(def, :sensor_type)
            assert Map.has_key?(def, :sensor_unit)
            assert Map.has_key?(def, :sensor_divisor)
          end)
        end
      end

      describe "discover_wireless_sensors/1" do
        test "discovers sensors when SNMP responds" do
          defs = #{module_name}.wireless_oid_defs()
          num_defs = length(defs)

          expect(SnmpMock, :get, num_defs, fn _, _oid, _ ->
            {:ok, 42}
          end)

          sensors = #{module_name}.discover_wireless_sensors(@client_opts)

          assert is_list(sensors)
          assert length(sensors) == num_defs
        end

        test "returns empty list when no sensors respond" do
          defs = #{module_name}.wireless_oid_defs()
          num_defs = length(defs)

          expect(SnmpMock, :get, num_defs, fn _, _, _ ->
            {:error, :no_such_object}
          end)

          sensors = #{module_name}.discover_wireless_sensors(@client_opts)

          assert sensors == []
        end
      end
    end
    """
  end
end
