//go:generate go run -tags generate snmp_mocks_generate.go
package snmp

import (
	"fmt"
	"net"
	"os/exec"
	"sync"
	"testing"
	"time"

	"github.com/influxdata/telegraf/internal"
	"github.com/influxdata/telegraf/testutil"
	"github.com/influxdata/toml"
	"github.com/soniah/gosnmp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type testSNMPConnection struct {
	host   string
	values map[string]interface{}
}

func (tsc *testSNMPConnection) Host() string {
	return tsc.host
}

func (tsc *testSNMPConnection) Get(oids []string) (*gosnmp.SnmpPacket, error) {
	sp := &gosnmp.SnmpPacket{}
	for _, oid := range oids {
		v, ok := tsc.values[oid]
		if !ok {
			sp.Variables = append(sp.Variables, gosnmp.SnmpPDU{
				Name: oid,
				Type: gosnmp.NoSuchObject,
			})
			continue
		}
		sp.Variables = append(sp.Variables, gosnmp.SnmpPDU{
			Name:  oid,
			Value: v,
		})
	}
	return sp, nil
}
func (tsc *testSNMPConnection) Walk(oid string, wf gosnmp.WalkFunc) error {
	for void, v := range tsc.values {
		if void == oid || (len(void) > len(oid) && void[:len(oid)+1] == oid+".") {
			if err := wf(gosnmp.SnmpPDU{
				Name:  void,
				Value: v,
			}); err != nil {
				return err
			}
		}
	}
	return nil
}

var tsc = &testSNMPConnection{
	host: "tsc",
	values: map[string]interface{}{
		".1.0.0.0.1.1.0":     "foo",
		".1.0.0.0.1.1.1":     []byte("bar"),
		".1.0.0.0.1.1.2":     []byte(""),
		".1.0.0.0.1.102":     "bad",
		".1.0.0.0.1.2.0":     1,
		".1.0.0.0.1.2.1":     2,
		".1.0.0.0.1.2.2":     0,
		".1.0.0.0.1.3.0":     "0.123",
		".1.0.0.0.1.3.1":     "0.456",
		".1.0.0.0.1.3.2":     "0.000",
		".1.0.0.0.1.3.3":     "9.999",
		".1.0.0.0.1.5.0":     123456,
		".1.0.0.1.1":         "baz",
		".1.0.0.1.2":         234,
		".1.0.0.1.3":         []byte("byte slice"),
		".1.0.0.2.1.5.0.9.9": 11,
		".1.0.0.2.1.5.1.9.9": 22,
	},
}

func TestSampleConfig(t *testing.T) {
	conf := struct {
		Inputs struct {
			Snmp []*Snmp
		}
	}{}
	err := toml.Unmarshal([]byte("[[inputs.snmp]]\n"+(*Snmp)(nil).SampleConfig()), &conf)
	assert.NoError(t, err)

	s := Snmp{
		Agents:         []string{"127.0.0.1:161"},
		Timeout:        internal.Duration{Duration: 5 * time.Second},
		Version:        2,
		Community:      "public",
		MaxRepetitions: 10,
		Retries:        3,

		Name: "system",
		Fields: []Field{
			{Name: "hostname", Oid: ".1.0.0.1.1"},
			{Name: "uptime", Oid: ".1.0.0.1.2"},
			{Name: "load", Oid: ".1.0.0.1.3"},
			{Oid: "HOST-RESOURCES-MIB::hrMemorySize"},
		},
		Tables: []Table{
			{
				Name:        "remote_servers",
				InheritTags: []string{"hostname"},
				Fields: []Field{
					{Name: "server", Oid: ".1.0.0.0.1.0", IsTag: true},
					{Name: "connections", Oid: ".1.0.0.0.1.1"},
					{Name: "latency", Oid: ".1.0.0.0.1.2"},
				},
			},
			{
				Oid: "HOST-RESOURCES-MIB::hrNetworkTable",
			},
		},
	}
	assert.Equal(t, &s, conf.Inputs.Snmp[0])
}

func TestFieldInit(t *testing.T) {
	translations := []struct {
		inputOid           string
		inputName          string
		inputConversion    string
		expectedOid        string
		expectedName       string
		expectedConversion string
	}{
		{".1.2.3", "foo", "", ".1.2.3", "foo", ""},
		{".iso.2.3", "foo", "", ".1.2.3", "foo", ""},
		{".1.0.0.0.1.1", "", "", ".1.0.0.0.1.1", "server", ""},
		{".1.0.0.0.1.1.0", "", "", ".1.0.0.0.1.1.0", "server.0", ""},
		{".999", "", "", ".999", ".999", ""},
		{"TEST::server", "", "", ".1.0.0.0.1.1", "server", ""},
		{"TEST::server.0", "", "", ".1.0.0.0.1.1.0", "server.0", ""},
		{"TEST::server", "foo", "", ".1.0.0.0.1.1", "foo", ""},
		{"IF-MIB::ifPhysAddress.1", "", "", ".1.3.6.1.2.1.2.2.1.6.1", "ifPhysAddress.1", "hwaddr"},
		{"IF-MIB::ifPhysAddress.1", "", "none", ".1.3.6.1.2.1.2.2.1.6.1", "ifPhysAddress.1", "none"},
		{"BRIDGE-MIB::dot1dTpFdbAddress.1", "", "", ".1.3.6.1.2.1.17.4.3.1.1.1", "dot1dTpFdbAddress.1", "hwaddr"},
		{"TCP-MIB::tcpConnectionLocalAddress.1", "", "", ".1.3.6.1.2.1.6.19.1.2.1", "tcpConnectionLocalAddress.1", "ipaddr"},
	}

	for _, txl := range translations {
		f := Field{Oid: txl.inputOid, Name: txl.inputName, Conversion: txl.inputConversion}
		err := f.init()
		if !assert.NoError(t, err, "inputOid='%s' inputName='%s'", txl.inputOid, txl.inputName) {
			continue
		}
		assert.Equal(t, txl.expectedOid, f.Oid, "inputOid='%s' inputName='%s' inputConversion='%s'", txl.inputOid, txl.inputName, txl.inputConversion)
		assert.Equal(t, txl.expectedName, f.Name, "inputOid='%s' inputName='%s' inputConversion='%s'", txl.inputOid, txl.inputName, txl.inputConversion)
	}
}

func TestTableInit(t *testing.T) {
	tbl := Table{
		Oid: ".1.0.0.0",
		Fields: []Field{
			{Oid: ".999", Name: "foo"},
			{Oid: "TEST::description", Name: "description", IsTag: true},
		},
	}
	err := tbl.init()
	require.NoError(t, err)

	assert.Equal(t, "testTable", tbl.Name)

	assert.Len(t, tbl.Fields, 5)
	assert.Contains(t, tbl.Fields, Field{Oid: ".999", Name: "foo", initialized: true})
	assert.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.1", Name: "server", IsTag: true, initialized: true})
	assert.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.2", Name: "connections", initialized: true})
	assert.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.3", Name: "latency", initialized: true})
	assert.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.4", Name: "description", IsTag: true, initialized: true})
}

func TestSnmpInit(t *testing.T) {
	s := &Snmp{
		Tables: []Table{
			{Oid: "TEST::testTable"},
		},
		Fields: []Field{
			{Oid: "TEST::hostname"},
		},
	}

	err := s.init()
	require.NoError(t, err)

	assert.Len(t, s.Tables[0].Fields, 4)
	assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.1", Name: "server", IsTag: true, initialized: true})
	assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.2", Name: "connections", initialized: true})
	assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.3", Name: "latency", initialized: true})
	assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.4", Name: "description", initialized: true})

	assert.Equal(t, Field{
		Oid:         ".1.0.0.1.1",
		Name:        "hostname",
		initialized: true,
	}, s.Fields[0])
}

func TestSnmpInit_noTranslate(t *testing.T) {
	// override execCommand so it returns exec.ErrNotFound
	defer func(ec func(string, ...string) *exec.Cmd) { execCommand = ec }(execCommand)
	execCommand = func(_ string, _ ...string) *exec.Cmd {
		return exec.Command("snmptranslateExecErrNotFound")
	}

	s := &Snmp{
		Fields: []Field{
			{Oid: ".1.1.1.1", Name: "one", IsTag: true},
			{Oid: ".1.1.1.2", Name: "two"},
			{Oid: ".1.1.1.3"},
		},
		Tables: []Table{
			{Fields: []Field{
				{Oid: ".1.1.1.4", Name: "four", IsTag: true},
				{Oid: ".1.1.1.5", Name: "five"},
				{Oid: ".1.1.1.6"},
			}},
		},
	}

	err := s.init()
	require.NoError(t, err)

	assert.Equal(t, ".1.1.1.1", s.Fields[0].Oid)
	assert.Equal(t, "one", s.Fields[0].Name)
	assert.Equal(t, true, s.Fields[0].IsTag)

	assert.Equal(t, ".1.1.1.2", s.Fields[1].Oid)
	assert.Equal(t, "two", s.Fields[1].Name)
	assert.Equal(t, false, s.Fields[1].IsTag)

	assert.Equal(t, ".1.1.1.3", s.Fields[2].Oid)
	assert.Equal(t, ".1.1.1.3", s.Fields[2].Name)
	assert.Equal(t, false, s.Fields[2].IsTag)

	assert.Equal(t, ".1.1.1.4", s.Tables[0].Fields[0].Oid)
	assert.Equal(t, "four", s.Tables[0].Fields[0].Name)
	assert.Equal(t, true, s.Tables[0].Fields[0].IsTag)

	assert.Equal(t, ".1.1.1.5", s.Tables[0].Fields[1].Oid)
	assert.Equal(t, "five", s.Tables[0].Fields[1].Name)
	assert.Equal(t, false, s.Tables[0].Fields[1].IsTag)

	assert.Equal(t, ".1.1.1.6", s.Tables[0].Fields[2].Oid)
	assert.Equal(t, ".1.1.1.6", s.Tables[0].Fields[2].Name)
	assert.Equal(t, false, s.Tables[0].Fields[2].IsTag)
}

func TestGetSNMPConnection_v2(t *testing.T) {
	s := &Snmp{
		Agents:    []string{"1.2.3.4:567", "1.2.3.4"},
		Timeout:   internal.Duration{Duration: 3 * time.Second},
		Retries:   4,
		Version:   2,
		Community: "foo",
	}
	err := s.init()
	require.NoError(t, err)

	gsc, err := s.getConnection(0)
	require.NoError(t, err)
	gs := gsc.(gosnmpWrapper)
	assert.Equal(t, "1.2.3.4", gs.Target)
	assert.EqualValues(t, 567, gs.Port)
	assert.Equal(t, gosnmp.Version2c, gs.Version)
	assert.Equal(t, "foo", gs.Community)

	gsc, err = s.getConnection(1)
	require.NoError(t, err)
	gs = gsc.(gosnmpWrapper)
	assert.Equal(t, "1.2.3.4", gs.Target)
	assert.EqualValues(t, 161, gs.Port)
}

func TestGetSNMPConnection_v3(t *testing.T) {
	s := &Snmp{
		Agents:         []string{"1.2.3.4"},
		Version:        3,
		MaxRepetitions: 20,
		ContextName:    "mycontext",
		SecLevel:       "authPriv",
		SecName:        "myuser",
		AuthProtocol:   "md5",
		AuthPassword:   "password123",
		PrivProtocol:   "des",
		PrivPassword:   "321drowssap",
		EngineID:       "myengineid",
		EngineBoots:    1,
		EngineTime:     2,
	}
	err := s.init()
	require.NoError(t, err)

	gsc, err := s.getConnection(0)
	require.NoError(t, err)
	gs := gsc.(gosnmpWrapper)
	assert.Equal(t, gs.Version, gosnmp.Version3)
	sp := gs.SecurityParameters.(*gosnmp.UsmSecurityParameters)
	assert.Equal(t, "1.2.3.4", gsc.Host())
	assert.EqualValues(t, 20, gs.MaxRepetitions)
	assert.Equal(t, "mycontext", gs.ContextName)
	assert.Equal(t, gosnmp.AuthPriv, gs.MsgFlags&gosnmp.AuthPriv)
	assert.Equal(t, "myuser", sp.UserName)
	assert.Equal(t, gosnmp.MD5, sp.AuthenticationProtocol)
	assert.Equal(t, "password123", sp.AuthenticationPassphrase)
	assert.Equal(t, gosnmp.DES, sp.PrivacyProtocol)
	assert.Equal(t, "321drowssap", sp.PrivacyPassphrase)
	assert.Equal(t, "myengineid", sp.AuthoritativeEngineID)
	assert.EqualValues(t, 1, sp.AuthoritativeEngineBoots)
	assert.EqualValues(t, 2, sp.AuthoritativeEngineTime)
}

func TestGetSNMPConnection_caching(t *testing.T) {
	s := &Snmp{
		Agents: []string{"1.2.3.4", "1.2.3.5", "1.2.3.5"},
	}
	err := s.init()
	require.NoError(t, err)
	gs1, err := s.getConnection(0)
	require.NoError(t, err)
	gs2, err := s.getConnection(0)
	require.NoError(t, err)
	gs3, err := s.getConnection(1)
	require.NoError(t, err)
	gs4, err := s.getConnection(2)
	require.NoError(t, err)
	assert.True(t, gs1 == gs2)
	assert.False(t, gs2 == gs3)
	assert.False(t, gs3 == gs4)
}

func TestGosnmpWrapper_walk_retry(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping test due to random failures.")
	}
	srvr, err := net.ListenUDP("udp4", &net.UDPAddr{})
	defer srvr.Close()
	require.NoError(t, err)
	reqCount := 0
	// Set up a WaitGroup to wait for the server goroutine to exit and protect
	// reqCount.
	// Even though simultaneous access is impossible because the server will be
	// blocked on ReadFrom, without this the race detector gets unhappy.
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		defer wg.Done()
		buf := make([]byte, 256)
		for {
			_, addr, err := srvr.ReadFrom(buf)
			if err != nil {
				return
			}
			reqCount++

			srvr.WriteTo([]byte{'X'}, addr) // will cause decoding error
		}
	}()

	gs := &gosnmp.GoSNMP{
		Target:    srvr.LocalAddr().(*net.UDPAddr).IP.String(),
		Port:      uint16(srvr.LocalAddr().(*net.UDPAddr).Port),
		Version:   gosnmp.Version2c,
		Community: "public",
		Timeout:   time.Millisecond * 10,
		Retries:   1,
	}
	err = gs.Connect()
	require.NoError(t, err)
	conn := gs.Conn

	gsw := gosnmpWrapper{gs}
	err = gsw.Walk(".1.0.0", func(_ gosnmp.SnmpPDU) error { return nil })
	srvr.Close()
	wg.Wait()
	assert.Error(t, err)
	assert.False(t, gs.Conn == conn)
	assert.Equal(t, (gs.Retries+1)*2, reqCount)
}

func TestGosnmpWrapper_get_retry(t *testing.T) {
	// TODO: Fix this test
	t.Skip("Test failing too often, skip for now and revisit later.")
	srvr, err := net.ListenUDP("udp4", &net.UDPAddr{})
	defer srvr.Close()
	require.NoError(t, err)
	reqCount := 0
	// Set up a WaitGroup to wait for the server goroutine to exit and protect
	// reqCount.
	// Even though simultaneous access is impossible because the server will be
	// blocked on ReadFrom, without this the race detector gets unhappy.
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		defer wg.Done()
		buf := make([]byte, 256)
		for {
			_, addr, err := srvr.ReadFrom(buf)
			if err != nil {
				return
			}
			reqCount++

			srvr.WriteTo([]byte{'X'}, addr) // will cause decoding error
		}
	}()

	gs := &gosnmp.GoSNMP{
		Target:    srvr.LocalAddr().(*net.UDPAddr).IP.String(),
		Port:      uint16(srvr.LocalAddr().(*net.UDPAddr).Port),
		Version:   gosnmp.Version2c,
		Community: "public",
		Timeout:   time.Millisecond * 10,
		Retries:   1,
	}
	err = gs.Connect()
	require.NoError(t, err)
	conn := gs.Conn

	gsw := gosnmpWrapper{gs}
	_, err = gsw.Get([]string{".1.0.0"})
	srvr.Close()
	wg.Wait()
	assert.Error(t, err)
	assert.False(t, gs.Conn == conn)
	assert.Equal(t, (gs.Retries+1)*2, reqCount)
}

func TestTableBuild_walk(t *testing.T) {
	tbl := Table{
		Name:       "mytable",
		IndexAsTag: true,
		Fields: []Field{
			{
				Name:  "myfield1",
				Oid:   ".1.0.0.0.1.1",
				IsTag: true,
			},
			{
				Name: "myfield2",
				Oid:  ".1.0.0.0.1.2",
			},
			{
				Name:       "myfield3",
				Oid:        ".1.0.0.0.1.3",
				Conversion: "float",
			},
			{
				Name:           "myfield4",
				Oid:            ".1.0.0.2.1.5",
				OidIndexSuffix: ".9.9",
			},
			{
				Name:           "myfield5",
				Oid:            ".1.0.0.2.1.5",
				OidIndexLength: 1,
			},
		},
	}

	tb, err := tbl.Build(tsc, true)
	require.NoError(t, err)

	assert.Equal(t, tb.Name, "mytable")
	rtr1 := RTableRow{
		Tags: map[string]string{
			"myfield1": "foo",
			"index":    "0",
		},
		Fields: map[string]interface{}{
			"myfield2": 1,
			"myfield3": float64(0.123),
			"myfield4": 11,
			"myfield5": 11,
		},
	}
	rtr2 := RTableRow{
		Tags: map[string]string{
			"myfield1": "bar",
			"index":    "1",
		},
		Fields: map[string]interface{}{
			"myfield2": 2,
			"myfield3": float64(0.456),
			"myfield4": 22,
			"myfield5": 22,
		},
	}
	rtr3 := RTableRow{
		Tags: map[string]string{
			"index": "2",
		},
		Fields: map[string]interface{}{
			"myfield2": 0,
			"myfield3": float64(0.0),
		},
	}
	rtr4 := RTableRow{
		Tags: map[string]string{
			"index": "3",
		},
		Fields: map[string]interface{}{
			"myfield3": float64(9.999),
		},
	}
	assert.Len(t, tb.Rows, 4)
	assert.Contains(t, tb.Rows, rtr1)
	assert.Contains(t, tb.Rows, rtr2)
	assert.Contains(t, tb.Rows, rtr3)
	assert.Contains(t, tb.Rows, rtr4)
}

func TestTableBuild_noWalk(t *testing.T) {
	tbl := Table{
		Name: "mytable",
		Fields: []Field{
			{
				Name:  "myfield1",
				Oid:   ".1.0.0.1.1",
				IsTag: true,
			},
			{
				Name: "myfield2",
				Oid:  ".1.0.0.1.2",
			},
			{
				Name:  "myfield3",
				Oid:   ".1.0.0.1.2",
				IsTag: true,
			},
			{
				Name: "empty",
				Oid:  ".1.0.0.0.1.1.2",
			},
			{
				Name: "noexist",
				Oid:  ".1.2.3.4.5",
			},
		},
	}

	tb, err := tbl.Build(tsc, false)
	require.NoError(t, err)

	rtr := RTableRow{
		Tags:   map[string]string{"myfield1": "baz", "myfield3": "234"},
		Fields: map[string]interface{}{"myfield2": 234},
	}
	assert.Len(t, tb.Rows, 1)
	assert.Contains(t, tb.Rows, rtr)
}

func TestGather(t *testing.T) {
	s := &Snmp{
		Agents: []string{"TestGather"},
		Name:   "mytable",
		Fields: []Field{
			{
				Name:  "myfield1",
				Oid:   ".1.0.0.1.1",
				IsTag: true,
			},
			{
				Name: "myfield2",
				Oid:  ".1.0.0.1.2",
			},
			{
				Name: "myfield3",
				Oid:  "1.0.0.1.1",
			},
		},
		Tables: []Table{
			{
				Name:        "myOtherTable",
				InheritTags: []string{"myfield1"},
				Fields: []Field{
					{
						Name: "myOtherField",
						Oid:  ".1.0.0.0.1.5",
					},
				},
			},
		},

		connectionCache: []snmpConnection{
			tsc,
		},
		initialized: true,
	}
	acc := &testutil.Accumulator{}

	tstart := time.Now()
	s.Gather(acc)
	tstop := time.Now()

	require.Len(t, acc.Metrics, 2)

	m := acc.Metrics[0]
	assert.Equal(t, "mytable", m.Measurement)
	assert.Equal(t, "tsc", m.Tags["agent_host"])
	assert.Equal(t, "baz", m.Tags["myfield1"])
	assert.Len(t, m.Fields, 2)
	assert.Equal(t, 234, m.Fields["myfield2"])
	assert.Equal(t, "baz", m.Fields["myfield3"])
	assert.True(t, tstart.Before(m.Time))
	assert.True(t, tstop.After(m.Time))

	m2 := acc.Metrics[1]
	assert.Equal(t, "myOtherTable", m2.Measurement)
	assert.Equal(t, "tsc", m2.Tags["agent_host"])
	assert.Equal(t, "baz", m2.Tags["myfield1"])
	assert.Len(t, m2.Fields, 1)
	assert.Equal(t, 123456, m2.Fields["myOtherField"])
}

func TestGather_host(t *testing.T) {
	s := &Snmp{
		Agents: []string{"TestGather"},
		Name:   "mytable",
		Fields: []Field{
			{
				Name:  "host",
				Oid:   ".1.0.0.1.1",
				IsTag: true,
			},
			{
				Name: "myfield2",
				Oid:  ".1.0.0.1.2",
			},
		},

		connectionCache: []snmpConnection{
			tsc,
		},
		initialized: true,
	}

	acc := &testutil.Accumulator{}

	s.Gather(acc)

	require.Len(t, acc.Metrics, 1)
	m := acc.Metrics[0]
	assert.Equal(t, "baz", m.Tags["host"])
}

func TestFieldConvert(t *testing.T) {
	testTable := []struct {
		input    interface{}
		conv     string
		expected interface{}
	}{
		{[]byte("foo"), "", string("foo")},
		{"0.123", "float", float64(0.123)},
		{[]byte("0.123"), "float", float64(0.123)},
		{float32(0.123), "float", float64(float32(0.123))},
		{float64(0.123), "float", float64(0.123)},
		{123, "float", float64(123)},
		{123, "float(0)", float64(123)},
		{123, "float(4)", float64(0.0123)},
		{int8(123), "float(3)", float64(0.123)},
		{int16(123), "float(3)", float64(0.123)},
		{int32(123), "float(3)", float64(0.123)},
		{int64(123), "float(3)", float64(0.123)},
		{uint(123), "float(3)", float64(0.123)},
		{uint8(123), "float(3)", float64(0.123)},
		{uint16(123), "float(3)", float64(0.123)},
		{uint32(123), "float(3)", float64(0.123)},
		{uint64(123), "float(3)", float64(0.123)},
		{"123", "int", int64(123)},
		{[]byte("123"), "int", int64(123)},
		{float32(12.3), "int", int64(12)},
		{float64(12.3), "int", int64(12)},
		{int(123), "int", int64(123)},
		{int8(123), "int", int64(123)},
		{int16(123), "int", int64(123)},
		{int32(123), "int", int64(123)},
		{int64(123), "int", int64(123)},
		{uint(123), "int", int64(123)},
		{uint8(123), "int", int64(123)},
		{uint16(123), "int", int64(123)},
		{uint32(123), "int", int64(123)},
		{uint64(123), "int", int64(123)},
		{[]byte("abcdef"), "hwaddr", "61:62:63:64:65:66"},
		{"abcdef", "hwaddr", "61:62:63:64:65:66"},
		{[]byte("abcd"), "ipaddr", "97.98.99.100"},
		{"abcd", "ipaddr", "97.98.99.100"},
		{[]byte("abcdefghijklmnop"), "ipaddr", "6162:6364:6566:6768:696a:6b6c:6d6e:6f70"},
	}

	for _, tc := range testTable {
		act, err := fieldConvert(tc.conv, tc.input)
		if !assert.NoError(t, err, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected) {
			continue
		}
		assert.EqualValues(t, tc.expected, act, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected)
	}
}

func TestSnmpTranslateCache_miss(t *testing.T) {
	snmpTranslateCaches = nil
	oid := "IF-MIB::ifPhysAddress.1"
	mibName, oidNum, oidText, conversion, err := snmpTranslate(oid)
	assert.Len(t, snmpTranslateCaches, 1)
	stc := snmpTranslateCaches[oid]
	require.NotNil(t, stc)
	assert.Equal(t, mibName, stc.mibName)
	assert.Equal(t, oidNum, stc.oidNum)
	assert.Equal(t, oidText, stc.oidText)
	assert.Equal(t, conversion, stc.conversion)
	assert.Equal(t, err, stc.err)
}

func TestSnmpTranslateCache_hit(t *testing.T) {
	snmpTranslateCaches = map[string]snmpTranslateCache{
		"foo": {
			mibName:    "a",
			oidNum:     "b",
			oidText:    "c",
			conversion: "d",
			err:        fmt.Errorf("e"),
		},
	}
	mibName, oidNum, oidText, conversion, err := snmpTranslate("foo")
	assert.Equal(t, "a", mibName)
	assert.Equal(t, "b", oidNum)
	assert.Equal(t, "c", oidText)
	assert.Equal(t, "d", conversion)
	assert.Equal(t, fmt.Errorf("e"), err)
	snmpTranslateCaches = nil
}

func TestSnmpTableCache_miss(t *testing.T) {
	snmpTableCaches = nil
	oid := ".1.0.0.0"
	mibName, oidNum, oidText, fields, err := snmpTable(oid)
	assert.Len(t, snmpTableCaches, 1)
	stc := snmpTableCaches[oid]
	require.NotNil(t, stc)
	assert.Equal(t, mibName, stc.mibName)
	assert.Equal(t, oidNum, stc.oidNum)
	assert.Equal(t, oidText, stc.oidText)
	assert.Equal(t, fields, stc.fields)
	assert.Equal(t, err, stc.err)
}

func TestSnmpTableCache_hit(t *testing.T) {
	snmpTableCaches = map[string]snmpTableCache{
		"foo": {
			mibName: "a",
			oidNum:  "b",
			oidText: "c",
			fields:  []Field{{Name: "d"}},
			err:     fmt.Errorf("e"),
		},
	}
	mibName, oidNum, oidText, fields, err := snmpTable("foo")
	assert.Equal(t, "a", mibName)
	assert.Equal(t, "b", oidNum)
	assert.Equal(t, "c", oidText)
	assert.Equal(t, []Field{{Name: "d"}}, fields)
	assert.Equal(t, fmt.Errorf("e"), err)
}

func TestError(t *testing.T) {
	e := fmt.Errorf("nested error")
	err := Errorf(e, "top error %d", 123)
	require.Error(t, err)

	ne, ok := err.(NestedError)
	require.True(t, ok)
	assert.Equal(t, e, ne.NestedErr)

	assert.Contains(t, err.Error(), "top error 123")
	assert.Contains(t, err.Error(), "nested error")
}