package snmp_trap

import (
	"fmt"
	"net"
	"strconv"
	"testing"
	"time"

	"github.com/soniah/gosnmp"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/internal"
	"github.com/influxdata/telegraf/testutil"

	"github.com/stretchr/testify/require"
)

func TestLoad(t *testing.T) {
	s := &SnmpTrap{}
	require.Nil(t, s.Init())

	defer s.clear()
	s.load(
		".1.3.6.1.6.3.1.1.5.1",
		mibEntry{
			"SNMPv2-MIB",
			"coldStart",
		},
	)

	e, err := s.lookup(".1.3.6.1.6.3.1.1.5.1")
	require.NoError(t, err)
	require.Equal(t, "SNMPv2-MIB", e.mibName)
	require.Equal(t, "coldStart", e.oidText)
}

func sendTrap(t *testing.T, port uint16) (sentTimestamp uint32) {
	s := &gosnmp.GoSNMP{
		Port:      port,
		Community: "public",
		Version:   gosnmp.Version2c,
		Timeout:   time.Duration(2) * time.Second,
		Retries:   3,
		MaxOids:   gosnmp.MaxOids,
		Target:    "127.0.0.1",
	}

	err := s.Connect()
	if err != nil {
		t.Errorf("Connect() err: %v", err)
	}
	defer s.Conn.Close()

	// If the first pdu isn't type TimeTicks, gosnmp.SendTrap() will
	// prepend one with time.Now().  The time value is part of the
	// plugin output so we need to keep track of it and verify it
	// later.
	now := uint32(time.Now().Unix())
	timePdu := gosnmp.SnmpPDU{
		Name:  ".1.3.6.1.2.1.1.3.0",
		Type:  gosnmp.TimeTicks,
		Value: now,
	}

	pdu := gosnmp.SnmpPDU{
		Name:  ".1.3.6.1.6.3.1.1.4.1.0", // SNMPv2-MIB::snmpTrapOID.0
		Type:  gosnmp.ObjectIdentifier,
		Value: ".1.3.6.1.6.3.1.1.5.1", // coldStart
	}

	trap := gosnmp.SnmpTrap{
		Variables: []gosnmp.SnmpPDU{
			timePdu,
			pdu,
		},
	}

	_, err = s.SendTrap(trap)
	if err != nil {
		t.Errorf("SendTrap() err: %v", err)
	}

	return now
}

func TestReceiveTrap(t *testing.T) {
	// We would prefer to specify port 0 and let the network stack
	// choose an unused port for us but TrapListener doesn't have a
	// way to return the autoselected port.  Instead, we'll use an
	// unusual port and hope it's unused.
	const port = 12399
	var fakeTime = time.Now()

	// hook into the trap handler so the test knows when the trap has
	// been received
	received := make(chan int)
	wrap := func(f handler) handler {
		return func(p *gosnmp.SnmpPacket, a *net.UDPAddr) {
			f(p, a)
			received <- 0
		}
	}

	// set up the service input plugin
	s := &SnmpTrap{
		ServiceAddress:     "udp://:" + strconv.Itoa(port),
		makeHandlerWrapper: wrap,
		timeFunc: func() time.Time {
			return fakeTime
		},
		Log: testutil.Logger{},
	}
	require.Nil(t, s.Init())
	var acc testutil.Accumulator
	require.Nil(t, s.Start(&acc))
	defer s.Stop()

	// Preload the cache with the oids we'll use in this test so
	// snmptranslate and mibs don't need to be installed.
	defer s.clear()
	s.load(".1.3.6.1.6.3.1.1.4.1.0",
		mibEntry{
			"SNMPv2-MIB",
			"snmpTrapOID.0",
		})
	s.load(".1.3.6.1.6.3.1.1.5.1",
		mibEntry{
			"SNMPv2-MIB",
			"coldStart",
		})
	s.load(".1.3.6.1.2.1.1.3.0",
		mibEntry{
			"UNUSED_MIB_NAME",
			"sysUpTimeInstance",
		})

	// send the trap
	sentTimestamp := sendTrap(t, port)

	// wait for trap to be received
	select {
	case <-received:
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for trap to be received")
	}

	// verify plugin output
	expected := []telegraf.Metric{
		testutil.MustMetric(
			"snmp_trap", // name
			map[string]string{ // tags
				"oid":     ".1.3.6.1.6.3.1.1.5.1",
				"name":    "coldStart",
				"mib":     "SNMPv2-MIB",
				"version": "2c",
				"source":  "127.0.0.1",
			},
			map[string]interface{}{ // fields
				"sysUpTimeInstance": sentTimestamp,
			},
			fakeTime,
		),
	}

	testutil.RequireMetricsEqual(t,
		expected, acc.GetTelegrafMetrics(),
		testutil.SortMetrics())

}

func fakeExecCmd(_ internal.Duration, _ string, _ ...string) ([]byte, error) {
	return nil, fmt.Errorf("intentional failure")
}

func TestMissingOid(t *testing.T) {
	// should fail even if snmptranslate is installed
	const port = 12399
	var fakeTime = time.Now()

	received := make(chan int)
	wrap := func(f handler) handler {
		return func(p *gosnmp.SnmpPacket, a *net.UDPAddr) {
			f(p, a)
			received <- 0
		}
	}

	s := &SnmpTrap{
		ServiceAddress:     "udp://:" + strconv.Itoa(port),
		makeHandlerWrapper: wrap,
		timeFunc: func() time.Time {
			return fakeTime
		},
		Log: testutil.Logger{},
	}
	require.Nil(t, s.Init())
	var acc testutil.Accumulator
	require.Nil(t, s.Start(&acc))
	defer s.Stop()

	// make sure the cache is empty
	s.clear()

	// don't call the real snmptranslate
	s.execCmd = fakeExecCmd

	_ = sendTrap(t, port)

	select {
	case <-received:
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for trap to be received")
	}

	// oid lookup should fail so we shouldn't get a metric
	expected := []telegraf.Metric{}

	testutil.RequireMetricsEqual(t,
		expected, acc.GetTelegrafMetrics(),
		testutil.SortMetrics())
}

func sendV1Trap(t *testing.T, port uint16) (sentTimestamp uint) {
	s := &gosnmp.GoSNMP{
		Port:      port,
		Community: "public",
		Version:   gosnmp.Version1,
		Timeout:   time.Duration(2) * time.Second,
		Retries:   3,
		MaxOids:   gosnmp.MaxOids,
		Target:    "127.0.0.1",
	}

	err := s.Connect()
	if err != nil {
		t.Fatalf("Connect() err: %v", err)
	}
	defer s.Conn.Close()

	now := uint(time.Now().Unix())

	pdu := gosnmp.SnmpPDU{
		Name:  ".1.2.3.4.5",
		Type:  gosnmp.OctetString,
		Value: "payload",
	}

	trap := gosnmp.SnmpTrap{
		Variables:    []gosnmp.SnmpPDU{pdu},
		Enterprise:   ".1.2.3",
		AgentAddress: "10.20.30.40",
		GenericTrap:  6, // enterpriseSpecific
		SpecificTrap: 55,
		Timestamp:    now,
	}

	_, err = s.SendTrap(trap)
	if err != nil {
		t.Fatalf("SendTrap() err: %v", err)
	}

	return now
}

func TestReceiveV1Trap(t *testing.T) {
	const port = 12399
	var fakeTime = time.Now()

	received := make(chan int)
	wrap := func(f handler) handler {
		return func(p *gosnmp.SnmpPacket, a *net.UDPAddr) {
			f(p, a)
			received <- 0
		}
	}

	s := &SnmpTrap{
		ServiceAddress:     "udp://:" + strconv.Itoa(port),
		makeHandlerWrapper: wrap,
		timeFunc: func() time.Time {
			return fakeTime
		},
		Log: testutil.Logger{},
	}
	require.Nil(t, s.Init())
	var acc testutil.Accumulator
	require.Nil(t, s.Start(&acc))
	defer s.Stop()

	defer s.clear()
	s.load(".1.2.3.4.5",
		mibEntry{
			"valueMIB",
			"valueOID",
		})
	s.load(".1.2.3.0.55",
		mibEntry{
			"enterpriseMIB",
			"enterpriseOID",
		})

	sentTimestamp := sendV1Trap(t, port)

	select {
	case <-received:
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for trap to be received")
	}

	expected := []telegraf.Metric{
		testutil.MustMetric(
			"snmp_trap", // name
			map[string]string{ // tags
				"oid":           ".1.2.3.0.55",
				"name":          "enterpriseOID",
				"mib":           "enterpriseMIB",
				"version":       "1",
				"source":        "127.0.0.1",
				"agent_address": "10.20.30.40",
			},
			map[string]interface{}{ // fields
				"sysUpTimeInstance": sentTimestamp,
				"valueOID":          "payload",
			},
			fakeTime,
		),
	}

	testutil.RequireMetricsEqual(t,
		expected, acc.GetTelegrafMetrics(),
		testutil.SortMetrics())

}
