package main

import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"

	"github.com/mndrix/tap-go"
	rspec "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/opencontainers/runtime-tools/specerror"
	"github.com/opencontainers/runtime-tools/validation/util"
)

func printDiag(t *tap.T, diagActual, diagExpected, diagNsType string, errNs error) {
	specErr := specerror.NewError(specerror.NSInheritWithoutType,
		errNs, rspec.Version)
	diagnostic := map[string]string{
		"actual":         diagActual,
		"expected":       diagExpected,
		"namespace type": diagNsType,
		"level":          specErr.(*specerror.Error).Err.Level.String(),
		"reference":      specErr.(*specerror.Error).Err.Reference,
	}
	t.YAML(diagnostic)
}

func testNamespaceInheritType(t *tap.T) error {
	var errNs error
	diagActual := ""
	diagExpected := ""
	diagNsType := ""

	// To be able to print out diagnostics for all kinds of error cases
	// at the end of the tests, we make use of defer function. To do that,
	// each error handling routine should set diagActual, diagExpected,
	// diagNsType, and errNs, before returning an error.
	defer func() {
		if errNs != nil {
			printDiag(t, diagActual, diagExpected, diagNsType, errNs)
		}
	}()

	g, err := util.GetDefaultGenerator()
	if err != nil {
		errNs = fmt.Errorf("cannot get the default generator: %v", err)
		diagActual = fmt.Sprintf("err == %v", errNs)
		diagExpected = "err == nil"
		// NOTE: we don't have a namespace type
		return errNs
	}

	// Obtain a map for host (runtime) namespace, and remove every namespace
	// from the generated config, to be able to see if each container namespace
	// becomes inherited from its corresponding host namespace.
	hostNsPath := fmt.Sprintf("/proc/%d/ns", os.Getpid())
	hostNsInodes := map[string]string{}
	for _, nsName := range util.ProcNamespaces {
		nsPathAbs := filepath.Join(hostNsPath, nsName)
		nsInode, err := os.Readlink(nsPathAbs)
		if err != nil {
			errNs = fmt.Errorf("cannot resolve symlink %q: %v", nsPathAbs, err)
			diagActual = fmt.Sprintf("err == %v", errNs)
			diagExpected = "err == nil"
			diagNsType = nsName
			return errNs
		}
		hostNsInodes[nsName] = nsInode

		if err := g.RemoveLinuxNamespace(util.GetRuntimeToolsNamespace(nsName)); err != nil {
			errNs = fmt.Errorf("cannot remove namespace %s: %v", nsName, err)
			diagActual = fmt.Sprintf("err == %v", errNs)
			diagExpected = "err == nil"
			diagNsType = nsName
			return errNs
		}
	}

	// We need to remove hostname to avoid test failures when not creating UTS namespace
	g.RemoveHostname()

	err = util.RuntimeOutsideValidate(g, t, func(config *rspec.Spec, t *tap.T, state *rspec.State) error {
		containerNsPath := fmt.Sprintf("/proc/%d/ns", state.Pid)

		for _, nsName := range util.ProcNamespaces {
			nsPathAbs := filepath.Join(containerNsPath, nsName)
			nsInode, err := os.Readlink(nsPathAbs)
			if err != nil {
				errNs = fmt.Errorf("cannot resolve symlink %q: %v", nsPathAbs, err)
				diagActual = fmt.Sprintf("err == %v", errNs)
				diagExpected = "err == nil"
				diagNsType = nsName
				return errNs
			}

			t.Ok(hostNsInodes[nsName] == nsInode, fmt.Sprintf("inherit namespace %s without type", nsName))
			if hostNsInodes[nsName] != nsInode {
				// NOTE: for such inode match cases, we should print out diagnostics
				// for each case, not only at the end of tests. So we should simply
				// call once printDiag(), then continue testing next namespaces.
				// Thus we don't need to set diagActual, diagExpected, diagNsType, etc.
				printDiag(t, nsInode, hostNsInodes[nsName], nsName,
					fmt.Errorf("namespace %s (inode %s) does not inherit runtime namespace %s", nsName, nsInode, hostNsInodes[nsName]))
				continue
			}
		}

		return nil
	})
	if err != nil {
		errNs = fmt.Errorf("cannot run validation tests: %v", err)
	}

	return errNs
}

func main() {
	t := tap.New()
	t.Header(0)

	if "linux" != runtime.GOOS {
		t.Skip(1, fmt.Sprintf("linux-specific namespace test"))
	}

	err := testNamespaceInheritType(t)
	if err != nil {
		t.Fail(err.Error())
	}

	t.AutoPlan()
}
