require "inspec/resources/file"

# Usage:
# describe bridge('br0') do
#   it { should exist }
#   it { should have_interface 'eth0' }
# end

module Inspec::Resources
  class Bridge < Inspec.resource(1)
    name "bridge"
    supports platform: "unix"
    desc "Use the bridge InSpec audit resource to test basic network bridge properties, such as name, if an interface is defined, and the associations for any defined interface."
    example <<~EXAMPLE
      describe bridge 'br0' do
        it { should exist }
        it { should have_interface 'eth0' }
      end
    EXAMPLE

    def initialize(bridge_name)
      @bridge_name = bridge_name

      @bridge_provider = nil
      if inspec.os.linux?
        @bridge_provider = LinuxBridge.new(inspec)
      elsif inspec.os.windows?
        @bridge_provider = WindowsBridge.new(inspec)
      else
        skip_resource "The `bridge` resource is not supported on your OS yet."
      end
    end

    def exists?
      !bridge_info.nil? && !bridge_info[:name].nil?
    end

    def has_interface?(interface)
      return skip_resource "The `bridge` resource does not provide interface detection for Windows yet" if inspec.os.windows?

      bridge_info.nil? ? false : bridge_info[:interfaces].include?(interface)
    end

    def interfaces
      bridge_info.nil? ? nil : bridge_info[:interfaces]
    end

    def resource_id
      @bridge_name || "bridge"
    end

    def to_s
      "Bridge #{@bridge_name}"
    end

    private

    def bridge_info
      return @cache if defined?(@cache)

      @cache = @bridge_provider.bridge_info(@bridge_name) unless @bridge_provider.nil?
    end
  end

  class BridgeDetection
    attr_reader :inspec
    def initialize(inspec)
      @inspec = inspec
    end
  end

  # Linux Bridge
  # If /sys/class/net/{interface}/bridge exists then it must be a bridge
  # /sys/class/net/{interface}/brif contains the network interfaces
  # @see http://www.tldp.org/HOWTO/BRIDGE-STP-HOWTO/set-up-the-bridge.html
  # @see http://unix.stackexchange.com/questions/40560/how-to-know-if-a-network-interface-is-tap-tun-bridge-or-physical
  class LinuxBridge < BridgeDetection
    def bridge_info(bridge_name)
      # read bridge information
      bridge = inspec.file("/sys/class/net/#{bridge_name}/bridge").directory?
      return nil unless bridge

      # load interface names
      interfaces = inspec.command("ls -1 /sys/class/net/#{bridge_name}/brif/")
      interfaces = interfaces.stdout.chomp.split("\n")
      {
        name: bridge_name,
        interfaces: interfaces,
      }
    end
  end

  # Windows Bridge
  # select netadapter by adapter binding for windows
  # Get-NetAdapterBinding -ComponentID ms_bridge | Get-NetAdapter
  # @see https://technet.microsoft.com/en-us/library/jj130921(v=wps.630).aspx
  # RegKeys: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}
  class WindowsBridge < BridgeDetection
    def bridge_info(bridge_name)
      # find all bridge adapters
      cmd = inspec.command("Get-NetAdapterBinding -ComponentID ms_bridge | Get-NetAdapter | Select-Object -Property Name, InterfaceDescription | ConvertTo-Json")

      # filter network interface
      begin
        bridges = JSON.parse(cmd.stdout)
      rescue JSON::ParserError => _e
        return nil
      end

      # ensure we have an array of groups
      bridges = [bridges] unless bridges.is_a?(Array)

      # select the requested interface
      bridges = bridges.each_with_object([]) do |adapter, adapter_collection|
        # map object
        info = {
          name: adapter["Name"],
          interfaces: nil,
        }
        adapter_collection.push(info) if info[:name].casecmp(bridge_name) == 0
      end

      return nil if bridges.empty?

      warn "[Possible Error] detected multiple bridges interfaces with the name #{bridge_name}" if bridges.size > 1
      bridges[0]
    end
  end
end
