package sdk_test

import (
	"context"
	"os"
	"os/exec"
	"testing"
	"time"

	nuclei "github.com/projectdiscovery/nuclei/v3/lib"
	"github.com/projectdiscovery/utils/env"
	"github.com/stretchr/testify/require"
	"github.com/tarunKoyalwar/goleak"
)

var knownLeaks = []goleak.Option{
	goleak.Pretty(),
	// net/http transport maintains idle keep-alive connections whose goroutines
	// exit on idle timeout or explicit close - not real leaks.
	goleak.IgnoreAnyFunction("net/http.(*http2ClientConn).readLoop"),
	// expirable LRU cache creates a background goroutine for TTL expiration that persists
	// see: https://github.com/hashicorp/golang-lru/blob/770151e9c8cdfae1797826b7b74c33d6f103fbd8/expirable/expirable_lru.go#L79
	goleak.IgnoreAnyContainingPkg("github.com/hashicorp/golang-lru/v2/expirable"),
	goleak.IgnoreAnyFunction("net/http.(*persistConn).readLoop"),
	goleak.IgnoreAnyFunction("net/http.(*persistConn).writeLoop"),
}

func TestSimpleNuclei(t *testing.T) {
	fn := func() {
		defer func() {
			// resources like leveldb have a delay to commit in-memory resources
			// to disk, typically 1-2 seconds, so we wait for 2 seconds
			time.Sleep(2 * time.Second)
			goleak.VerifyNone(t, knownLeaks...)
		}()
		ne, err := nuclei.NewNucleiEngineCtx(
			context.TODO(),
			nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}), // filter dns templates
			nuclei.EnableStatsWithOpts(nuclei.StatsOptions{JSON: true}),
		)
		require.Nil(t, err)
		ne.LoadTargets([]string{"scanme.sh"}, false) // probe non http/https target is set to false here
		// when callback is nil it nuclei will print JSON output to stdout
		err = ne.ExecuteWithCallback(nil)
		require.Nil(t, err)
		defer ne.Close()
	}

	// this is shared test so needs to be run as separate process
	if env.GetEnvOrDefault("TestSimpleNuclei", false) {
		// run as new process
		cmd := exec.Command(os.Args[0], "-test.run=TestSimpleNuclei")
		cmd.Env = append(os.Environ(), "TestSimpleNuclei=true")
		out, err := cmd.CombinedOutput()
		if err != nil {
			t.Fatalf("process ran with error %s, output: %s", err, out)
		}
	} else {
		fn()
	}
}

func TestSimpleNucleiRemote(t *testing.T) {
	fn := func() {
		defer func() {
			// resources like leveldb have a delay to commit in-memory resources
			// to disk, typically 1-2 seconds, so we wait for 2 seconds
			time.Sleep(2 * time.Second)
			goleak.VerifyNone(t, knownLeaks...)
		}()
		ne, err := nuclei.NewNucleiEngineCtx(
			context.TODO(),
			nuclei.WithTemplatesOrWorkflows(
				nuclei.TemplateSources{
					RemoteTemplates: []string{"https://cloud.projectdiscovery.io/public/nameserver-fingerprint.yaml"},
				},
			),
		)
		require.Nil(t, err)
		ne.LoadTargets([]string{"scanme.sh"}, false) // probe non http/https target is set to false here
		err = ne.LoadAllTemplates()
		require.Nil(t, err, "could not load templates")
		// when callback is nil it nuclei will print JSON output to stdout
		err = ne.ExecuteWithCallback(nil)
		require.Nil(t, err)
		defer ne.Close()
	}
	// this is shared test so needs to be run as separate process
	if env.GetEnvOrDefault("TestSimpleNucleiRemote", false) {
		cmd := exec.Command(os.Args[0], "-test.run=TestSimpleNucleiRemote")
		cmd.Env = append(os.Environ(), "TestSimpleNucleiRemote=true")
		out, err := cmd.CombinedOutput()
		if err != nil {
			t.Fatalf("process ran with error %s, output: %s", err, out)
		}
	} else {
		fn()
	}
}

func TestThreadSafeNuclei(t *testing.T) {
	fn := func() {
		defer func() {
			// resources like leveldb have a delay to commit in-memory resources
			// to disk, typically 1-2 seconds, so we wait for 2 seconds
			time.Sleep(2 * time.Second)
			goleak.VerifyNone(t, knownLeaks...)
		}()
		// create nuclei engine with options
		ne, err := nuclei.NewThreadSafeNucleiEngineCtx(context.TODO())
		require.Nil(t, err)

		// scan 1 = run dns templates on scanme.sh
		t.Run("scanme.sh", func(t *testing.T) {
			err = ne.ExecuteNucleiWithOpts([]string{"scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}))
			require.Nil(t, err)
		})

		// scan 2 = run dns templates on honey.scanme.sh
		t.Run("honey.scanme.sh", func(t *testing.T) {
			err = ne.ExecuteNucleiWithOpts([]string{"honey.scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}))
			require.Nil(t, err)
		})

		// wait for all scans to finish
		defer ne.Close()
	}

	if env.GetEnvOrDefault("TestThreadSafeNuclei", false) {
		cmd := exec.Command(os.Args[0], "-test.run=TestThreadSafeNuclei")
		cmd.Env = append(os.Environ(), "TestThreadSafeNuclei=true")
		out, err := cmd.CombinedOutput()
		if err != nil {
			t.Fatalf("process ran with error %s, output: %s", err, out)
		}
	} else {
		fn()
	}
}

func TestWithVarsNuclei(t *testing.T) {
	fn := func() {
		defer func() {
			// resources like leveldb have a delay to commit in-memory resources
			// to disk, typically 1-2 seconds, so we wait for 2 seconds
			time.Sleep(2 * time.Second)
			goleak.VerifyNone(t, knownLeaks...)
		}()
		ne, err := nuclei.NewNucleiEngineCtx(
			context.TODO(),
			nuclei.EnableSelfContainedTemplates(),
			nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{"http/token-spray/api-1forge.yaml"}}),
			nuclei.WithVars([]string{"token=foobar"}),
			nuclei.WithVerbosity(nuclei.VerbosityOptions{Debug: true}),
		)
		require.Nil(t, err)
		ne.LoadTargets([]string{"scanme.sh"}, true) // probe http/https target is set to true here
		err = ne.ExecuteWithCallback(nil)
		require.Nil(t, err)
		defer ne.Close()
	}
	// this is shared test so needs to be run as separate process
	if env.GetEnvOrDefault("TestWithVarsNuclei", false) {
		cmd := exec.Command(os.Args[0], "-test.run=TestWithVarsNuclei")
		cmd.Env = append(os.Environ(), "TestWithVarsNuclei=true")
		out, err := cmd.CombinedOutput()
		if err != nil {
			t.Fatalf("process ran with error %s, output: %s", err, out)
		}
	} else {
		fn()
	}
}
