package ebpf

import (
	"errors"
	"testing"

	"github.com/cilium/ebpf/asm"
	"github.com/cilium/ebpf/internal"
	"github.com/cilium/ebpf/internal/testutils"

	qt "github.com/frankban/quicktest"
)

func TestFindReferences(t *testing.T) {
	progs := map[string]*ProgramSpec{
		"entrypoint": {
			Type: SocketFilter,
			Instructions: asm.Instructions{
				// Make sure the call doesn't happen at instruction 0
				// to exercise the relative offset calculation.
				asm.Mov.Reg(asm.R0, asm.R1),
				asm.Call.Label("my_func"),
				asm.Return(),
			},
			License: "MIT",
		},
		"my_other_func": {
			Instructions: asm.Instructions{
				asm.LoadImm(asm.R0, 1337, asm.DWord).WithSymbol("my_other_func"),
				asm.Return(),
			},
		},
		"my_func": {
			Instructions: asm.Instructions{
				asm.Call.Label("my_other_func").WithSymbol("my_func"),
				asm.Return(),
			},
		},
	}

	flattenPrograms(progs, []string{"entrypoint"})

	prog, err := NewProgram(progs["entrypoint"])
	testutils.SkipIfNotSupported(t, err)
	if err != nil {
		t.Fatal(err)
	}
	defer prog.Close()

	ret, _, err := prog.Test(make([]byte, 14))
	if err != nil {
		t.Fatal(err)
	}

	if ret != 1337 {
		t.Errorf("Expected return code 1337, got %d", ret)
	}
}

func TestForwardFunctionDeclaration(t *testing.T) {
	testutils.Files(t, testutils.Glob(t, "testdata/fwd_decl-*.elf"), func(t *testing.T, file string) {
		coll, err := LoadCollectionSpec(file)
		if err != nil {
			t.Fatal(err)
		}

		if coll.ByteOrder != internal.NativeEndian {
			return
		}

		spec := coll.Programs["call_fwd"]

		// This program calls an unimplemented forward function declaration.
		_, err = NewProgram(spec)
		if !errors.Is(err, asm.ErrUnsatisfiedProgramReference) {
			t.Fatal("Expected an error wrapping ErrUnsatisfiedProgramReference, got:", err)
		}

		// Append the implementation of fwd().
		spec.Instructions = append(spec.Instructions,
			asm.Mov.Imm32(asm.R0, 23).WithSymbol("fwd"),
			asm.Return(),
		)

		// The body of the subprog we appended does not come with BTF func_infos,
		// so the verifier will reject it. Load without BTF.
		spec.BTF = nil

		prog, err := NewProgram(spec)
		testutils.SkipIfNotSupported(t, err)
		if err != nil {
			t.Fatal(err)
		}
		defer prog.Close()

		ret, _, err := prog.Test(make([]byte, 14))
		if err != nil {
			t.Fatal("Running program:", err)
		}
		if ret != 23 {
			t.Fatalf("Expected 23, got %d", ret)
		}
	})
}

func TestSplitSymbols(t *testing.T) {
	c := qt.New(t)

	// Splitting an empty insns results in an error.
	_, err := splitSymbols(asm.Instructions{})
	c.Assert(err, qt.IsNotNil, qt.Commentf("empty insns"))

	// Splitting non-empty insns without a leading Symbol is an error.
	_, err = splitSymbols(asm.Instructions{
		asm.Return(),
	})
	c.Assert(err, qt.IsNotNil, qt.Commentf("insns without leading Symbol"))

	// Non-empty insns with a single Instruction that is a Symbol.
	insns := asm.Instructions{
		asm.Return().WithSymbol("sym"),
	}
	m, err := splitSymbols(insns)
	c.Assert(err, qt.IsNil, qt.Commentf("insns with a single Symbol"))

	c.Assert(len(m), qt.Equals, 1)
	c.Assert(len(m["sym"]), qt.Equals, 1)

	// Insns containing duplicate Symbols.
	_, err = splitSymbols(asm.Instructions{
		asm.Return().WithSymbol("sym"),
		asm.Return().WithSymbol("sym"),
	})
	c.Assert(err, qt.IsNotNil, qt.Commentf("insns containing duplicate Symbols"))

	// Insns with multiple Symbols and subprogs of various lengths.
	m, err = splitSymbols(asm.Instructions{
		asm.Return().WithSymbol("sym1"),

		asm.Mov.Imm(asm.R0, 0).WithSymbol("sym2"),
		asm.Return(),

		asm.Mov.Imm(asm.R0, 0).WithSymbol("sym3"),
		asm.Mov.Imm(asm.R0, 1),
		asm.Return(),

		asm.Mov.Imm(asm.R0, 0).WithSymbol("sym4"),
		asm.Mov.Imm(asm.R0, 1),
		asm.Mov.Imm(asm.R0, 2),
		asm.Return(),
	})
	c.Assert(err, qt.IsNil, qt.Commentf("insns with multiple Symbols"))

	c.Assert(len(m), qt.Equals, 4)
	c.Assert(len(m["sym1"]), qt.Equals, 1)
	c.Assert(len(m["sym2"]), qt.Equals, 2)
	c.Assert(len(m["sym3"]), qt.Equals, 3)
	c.Assert(len(m["sym4"]), qt.Equals, 4)
}
