defmodule SnmpKit.SnmpLib.MIB.CompilerTest do
  @moduledoc """
  Tests for the MIB Compiler interface.

  The Compiler module provides a high-level interface to the MIB compilation
  pipeline, which is implemented via the Parser module using YACC-based parsing.
  """

  use ExUnit.Case, async: true

  alias SnmpKit.SnmpLib.MIB.Compiler
  alias SnmpKit.SnmpLib.MIB.Parser

  # Skip these tests if yecc is not available (requires :parsetools)
  @moduletag :yecc_required

  describe "compile_string/2" do
    test "compiles a minimal MIB successfully" do
      # A valid v1 MIB requires at least one definition
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN

      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }

      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert compiled.name == "TEST-MIB"
      assert compiled.format == :binary
      assert map_size(compiled.symbols) == 1
      assert Map.has_key?(compiled.symbols, "testNode")
      assert compiled.dependencies == []
    end

    test "compiles a MIB with imports" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN

      IMPORTS
          DisplayString FROM SNMPv2-TC
          enterprises FROM SNMPv2-SMI;

      testNode OBJECT IDENTIFIER ::= { enterprises 99999 }

      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert compiled.name == "TEST-MIB"
      assert compiled.dependencies == ["SNMPv2-TC", "SNMPv2-SMI"]
    end

    test "compiles a MIB with object definitions" do
      mib_content =
        "TEST-MIB DEFINITIONS ::= BEGIN\n" <>
          "testObject OBJECT-TYPE\n" <>
          "    SYNTAX INTEGER\n" <>
          "    MAX-ACCESS read-only\n" <>
          "    STATUS current\n" <>
          "    DESCRIPTION \"A test object\"\n" <>
          "    ::= { enterprises 12345 1 }\n" <>
          "END\n"

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert compiled.name == "TEST-MIB"
      assert map_size(compiled.symbols) > 0
      assert Map.has_key?(compiled.symbols, "testObject")
    end

    test "returns error for invalid MIB syntax" do
      mib_content = """
      INVALID MIB SYNTAX
      """

      assert {:error, errors} = Compiler.compile_string(mib_content)
      assert is_list(errors)
      assert errors != []
    end

    test "respects compile options" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN

      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }

      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content, format: :json)
      assert compiled.format == :json
    end
  end

  describe "compile/2" do
    test "compiles a MIB file from disk" do
      # Use a known good MIB file from fixtures
      mib_path = "test/fixtures/mibs/working/RFC1213-MIB.mib"

      if File.exists?(mib_path) do
        assert {:ok, compiled} = Compiler.compile(mib_path)
        assert compiled.name == "RFC1213-MIB"
        assert compiled.format == :binary
      else
        # Skip if fixtures aren't available
        :ok
      end
    end

    test "returns error for non-existent file" do
      assert {:error, errors} = Compiler.compile("non_existent.mib")
      assert is_list(errors)
      [error | _] = errors
      assert error.type == :file_not_found
    end
  end

  describe "integration with Parser" do
    test "Compiler and Parser produce compatible results" do
      mib_content =
        "TEST-MIB DEFINITIONS ::= BEGIN\n" <>
          "IMPORTS\n" <>
          "    DisplayString FROM SNMPv2-TC;\n" <>
          "\n" <>
          "testObject OBJECT-TYPE\n" <>
          "    SYNTAX DisplayString\n" <>
          "    MAX-ACCESS read-only\n" <>
          "    STATUS current\n" <>
          "    DESCRIPTION \"Test object\"\n" <>
          "    ::= { enterprises 12345 1 }\n" <>
          "END\n"

      # Test that Compiler properly wraps Parser functionality
      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert {:ok, parsed} = Parser.parse(mib_content)

      # Verify the compiler adds the expected structure
      assert compiled.name == parsed.name
      assert length(compiled.dependencies) == length(parsed.imports)
      assert map_size(compiled.symbols) == length(parsed.definitions)
    end
  end

  describe "compile_all/2" do
    test "compiles multiple MIBs successfully" do
      # Use minimal valid MIBs
      mib1 = """
      MIB-ONE DEFINITIONS ::= BEGIN
      nodeOne OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 1 }
      END
      """

      mib2 = """
      MIB-TWO DEFINITIONS ::= BEGIN
      nodeTwo OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 2 }
      END
      """

      # Write temp files
      dir = Path.join(System.tmp_dir!(), "test_mibs_#{:rand.uniform(1_000_000)}")
      File.mkdir_p!(dir)
      file1 = Path.join(dir, "mib-one.mib")
      file2 = Path.join(dir, "mib-two.mib")
      File.write!(file1, mib1)
      File.write!(file2, mib2)

      assert {:ok, compiled_list} = Compiler.compile_all([file1, file2])
      assert length(compiled_list) == 2
      assert Enum.any?(compiled_list, &(&1.name == "MIB-ONE"))
      assert Enum.any?(compiled_list, &(&1.name == "MIB-TWO"))

      # Cleanup
      File.rm_rf!(dir)
    end

    test "returns error when any MIB fails to compile" do
      valid_mib = """
      VALID-MIB DEFINITIONS ::= BEGIN
      validNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 1 }
      END
      """

      invalid_mib = "INVALID SYNTAX"

      dir = Path.join(System.tmp_dir!(), "test_mibs_#{:rand.uniform(1_000_000)}")
      File.mkdir_p!(dir)
      file1 = Path.join(dir, "valid.mib")
      file2 = Path.join(dir, "invalid.mib")
      File.write!(file1, valid_mib)
      File.write!(file2, invalid_mib)

      assert {:error, errors} = Compiler.compile_all([file1, file2])
      assert is_list(errors)
      refute Enum.empty?(errors)

      # Cleanup
      File.rm_rf!(dir)
    end

    test "handles empty file list" do
      assert {:ok, []} = Compiler.compile_all([])
    end

    test "continues compiling after encountering errors" do
      # Test that compile_all doesn't stop on first error
      dir = Path.join(System.tmp_dir!(), "test_mibs_#{:rand.uniform(1_000_000)}")
      File.mkdir_p!(dir)

      mib1 = """
      MIB-ONE DEFINITIONS ::= BEGIN
      nodeOne OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 1 }
      END
      """

      file1 = Path.join(dir, "valid1.mib")
      file2 = Path.join(dir, "invalid.mib")
      file3 = Path.join(dir, "valid2.mib")

      File.write!(file1, mib1)
      File.write!(file2, "INVALID")
      File.write!(file3, mib1)

      assert {:error, errors} = Compiler.compile_all([file1, file2, file3])

      # Should have one error (file2)
      assert length(errors) == 1
      [{error_file, _error_list}] = errors
      assert error_file == file2

      File.rm_rf!(dir)
    end
  end

  describe "load_compiled/1" do
    test "loads a previously compiled MIB" do
      # Create a compiled MIB
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      {:ok, compiled} = Compiler.compile_string(mib_content)

      # Save it to disk
      dir = Path.join(System.tmp_dir!(), "test_compiled_#{:rand.uniform(1_000_000)}")
      File.mkdir_p!(dir)
      path = Path.join(dir, "test.mib")

      compiled_with_type = Map.put(compiled, :__type__, :compiled_mib)
      File.write!(path, :erlang.term_to_binary(compiled_with_type))

      # Load it back
      assert {:ok, loaded} = Compiler.load_compiled(path)
      assert loaded.name == compiled.name
      assert loaded.__type__ == :compiled_mib

      File.rm_rf!(dir)
    end

    test "returns error for non-existent file" do
      assert {:error, :enoent} = Compiler.load_compiled("non_existent.mib")
    end

    test "returns error for invalid compiled format" do
      dir = Path.join(System.tmp_dir!(), "test_compiled_#{:rand.uniform(1_000_000)}")
      File.mkdir_p!(dir)
      path = Path.join(dir, "invalid.mib")

      # Write invalid data (not a compiled MIB)
      File.write!(path, :erlang.term_to_binary(%{random: "data"}))

      assert {:error, :invalid_compiled_format} = Compiler.load_compiled(path)

      File.rm_rf!(dir)
    end
  end

  describe "compile options" do
    test "validates option with validate: true" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, _compiled} = Compiler.compile_string(mib_content, validate: true)
    end

    test "skips validation with validate: false" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, _compiled} = Compiler.compile_string(mib_content, validate: false)
    end

    test "accepts format option" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content, format: :erlang)
      assert compiled.format == :erlang
    end

    test "accepts output_dir option" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, _compiled} = Compiler.compile_string(mib_content, output_dir: "/tmp/custom")
    end

    test "accepts optimize option" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, _compiled} = Compiler.compile_string(mib_content, optimize: false)
    end

    test "accepts include_paths option" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, _compiled} = Compiler.compile_string(mib_content, include_paths: ["/tmp/mibs"])
    end

    test "accepts warnings_as_errors option" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, _compiled} = Compiler.compile_string(mib_content, warnings_as_errors: true)
    end

    test "merges options with defaults" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      # Only specify format, should merge with defaults
      assert {:ok, compiled} = Compiler.compile_string(mib_content, format: :json)
      assert compiled.format == :json
    end
  end

  describe "error handling" do
    test "handles parser syntax errors gracefully" do
      mib_content = "INVALID SYNTAX"

      assert {:error, errors} = Compiler.compile_string(mib_content)
      assert is_list(errors)
      refute Enum.empty?(errors)
    end

    test "handles missing file errors" do
      assert {:error, errors} = Compiler.compile("non_existent.mib")
      [error | _] = errors
      assert error.type == :file_not_found
      assert String.contains?(error.message, "Could not read MIB file")
    end

    test "handles parse errors with line numbers" do
      # Incomplete MIB that should fail parsing
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      """

      assert {:error, errors} = Compiler.compile_string(mib_content)
      assert is_list(errors)
    end
  end

  describe "symbol table construction" do
    test "builds symbol table from definitions" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN

      nodeOne OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 1 }
      nodeTwo OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 2 }

      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert map_size(compiled.symbols) == 2
      assert Map.has_key?(compiled.symbols, "nodeOne")
      assert Map.has_key?(compiled.symbols, "nodeTwo")
    end

    test "symbol table contains definition data" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN

      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }

      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert Map.has_key?(compiled.symbols, "testNode")
      symbol = compiled.symbols["testNode"]
      assert symbol.name == "testNode"
    end
  end

  describe "dependency extraction" do
    test "extracts no dependencies from MIB without imports" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert compiled.dependencies == []
    end

    test "extracts single dependency" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN

      IMPORTS
          DisplayString FROM SNMPv2-TC;

      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }

      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert compiled.dependencies == ["SNMPv2-TC"]
    end

    test "extracts multiple dependencies" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN

      IMPORTS
          DisplayString FROM SNMPv2-TC
          enterprises FROM SNMPv2-SMI;

      testNode OBJECT IDENTIFIER ::= { enterprises 99999 }

      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert "SNMPv2-TC" in compiled.dependencies
      assert "SNMPv2-SMI" in compiled.dependencies
      assert length(compiled.dependencies) == 2
    end

    test "deduplicates dependencies" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN

      IMPORTS
          DisplayString, ObjectName FROM SNMPv2-TC
          enterprises FROM SNMPv2-SMI;

      testNode OBJECT IDENTIFIER ::= { enterprises 99999 }

      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert length(compiled.dependencies) == 2
    end
  end

  describe "compiled MIB structure" do
    test "includes all required fields" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert Map.has_key?(compiled, :name)
      assert Map.has_key?(compiled, :version)
      assert Map.has_key?(compiled, :format)
      assert Map.has_key?(compiled, :path)
      assert Map.has_key?(compiled, :metadata)
      assert Map.has_key?(compiled, :oid_tree)
      assert Map.has_key?(compiled, :symbols)
      assert Map.has_key?(compiled, :dependencies)
    end

    test "sets path to nil for string compilation" do
      mib_content = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 }
      END
      """

      assert {:ok, compiled} = Compiler.compile_string(mib_content)
      assert compiled.path == nil
    end
  end
end
