package integration

import (
	"context"
	"log"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/jarcoal/httpmock"

	"github.com/dnaeon/go-vcr/cassette"
	"github.com/dnaeon/go-vcr/recorder"
	"github.com/linode/linodego"
	"golang.org/x/oauth2"
	"k8s.io/client-go/transport"
)

var (
	testingMode     = recorder.ModeDisabled
	debugAPI        = false
	validTestAPIKey = "NOTANAPIKEY"
)

var (
	testingPollDuration = 15 * time.Second
	testingMaxRetryTime = 30 * time.Second
)

func init() {
	if apiToken, ok := os.LookupEnv("LINODE_TOKEN"); ok {
		validTestAPIKey = apiToken
	}

	if apiDebug, ok := os.LookupEnv("LINODE_DEBUG"); ok {
		if parsed, err := strconv.ParseBool(apiDebug); err == nil {
			debugAPI = parsed
			log.Println("[INFO] LINODE_DEBUG being set to", debugAPI)
		} else {
			log.Println("[WARN] LINODE_DEBUG should be an integer, 0 or 1")
		}
	}

	if envFixtureMode, ok := os.LookupEnv("LINODE_FIXTURE_MODE"); ok {
		if envFixtureMode == "record" {
			log.Printf("[INFO] LINODE_FIXTURE_MODE %s will be used for tests", envFixtureMode)
			testingMode = recorder.ModeRecording
		} else if envFixtureMode == "play" {
			log.Printf("[INFO] LINODE_FIXTURE_MODE %s will be used for tests", envFixtureMode)
			testingMode = recorder.ModeReplaying
			testingPollDuration = 1
			testingMaxRetryTime = time.Duration(1) * time.Microsecond
		}
	}
}

// testRecorder returns a go-vcr recorder and an associated function that the caller must defer
func testRecorder(t *testing.T, fixturesYaml string, testingMode recorder.Mode, realTransport http.RoundTripper) (r *recorder.Recorder, recordStopper func()) {
	if t != nil {
		t.Helper()
	}

	r, err := recorder.NewAsMode(fixturesYaml, testingMode, realTransport)
	if err != nil {
		log.Fatalln(err)
	}

	r.AddFilter(func(i *cassette.Interaction) error {
		delete(i.Request.Headers, "Authorization")
		return nil
	})

	r.AddFilter(func(i *cassette.Interaction) error {
		delete(i.Response.Headers, "Date")
		delete(i.Response.Headers, "Retry-After")
		delete(i.Response.Headers, "X-Customer-Uuid")
		delete(i.Response.Headers, "X-Ratelimit-Reset")
		delete(i.Response.Headers, "X-Ratelimit-Remaining")
		delete(i.Response.Headers, "X-Spec-Version")
		return nil
	})

	r.AddFilter(func(i *cassette.Interaction) error {
		re := regexp.MustCompile(`"access_key": "[[:alnum:]]*"`)
		i.Response.Body = re.ReplaceAllString(i.Response.Body, `"access_key": "[SANITIZED]"`)
		re = regexp.MustCompile(`"secret_key": "[[:alnum:]]*"`)
		i.Response.Body = re.ReplaceAllString(i.Response.Body, `"secret_key": "[SANITIZED]"`)
		re = regexp.MustCompile("20[0-9]{2}-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-9]{2}:[0-9]{2}")
		i.Response.Body = re.ReplaceAllString(i.Response.Body, "2018-01-02T03:04:05")
		// re = regexp.MustCompile("192\\.168\\.((1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\\.)(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])")
		// i.Response.Body = re.ReplaceAllString(i.Response.Body, "10.0.0.1")
		// re = regexp.MustCompile("^192\\.168/!s/((1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\\.){3}(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])")
		// i.Response.Body = re.ReplaceAllString(i.Response.Body, "0.0.0.0")
		re = regexp.MustCompile("nb-[0-9]{1,3}-[0-9]{1,3}-[0-9]{1,3}-[0-9]{1,3}")
		i.Response.Body = re.ReplaceAllString(i.Response.Body, "nb-0-0-0-0")
		return nil
	})

	r.AddSaveFilter(func(i *cassette.Interaction) error {
		re := regexp.MustCompile("AWSAccessKeyId=[[:alnum:]]{20}")
		i.Response.Body = re.ReplaceAllString(i.Response.Body, "AWSAccessKeyID=SANITIZED")
		i.Request.URL = re.ReplaceAllString(i.Request.URL, "AWSAccessKeyID=SANITIZED")
		return nil
	})

	recordStopper = func() {
		r.Stop()
	}
	return
}

// createTestClient is a testing helper to creates a linodego.Client initialized using
// environment variables and configured to record or playback testing fixtures.
// The returned function should be deferred by the caller to ensure the fixture
// recording is properly closed.
func createTestClient(t *testing.T, fixturesYaml string) (*linodego.Client, func()) {
	var (
		c      linodego.Client
		apiKey *string
	)
	if t != nil {
		t.Helper()
	}

	apiKey = &validTestAPIKey

	var recordStopper func()
	var r http.RoundTripper

	if len(fixturesYaml) > 0 {
		r, recordStopper = testRecorder(t, fixturesYaml, testingMode, nil)
	} else {
		r = nil
		recordStopper = func() {}
	}

	tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: *apiKey})
	oc := &http.Client{
		Transport: &oauth2.Transport{
			Source: tokenSource,
			Base:   r,
		},
	}

	c = linodego.NewClient(oc)
	c.SetDebug(debugAPI).
		SetPollDelay(testingPollDuration).
		SetRetryMaxWaitTime(testingMaxRetryTime)

	return &c, recordStopper
}

func createMockClient(t *testing.T) *linodego.Client {
	tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: validTestAPIKey})

	client := &http.Client{
		Transport: &oauth2.Transport{
			Source: tokenSource,
		},
	}
	httpmock.ActivateNonDefault(client)

	t.Cleanup(func() {
		httpmock.DeactivateAndReset()
	})

	c := linodego.NewClient(client)
	return &c
}

// transportRecordWrapper returns a tranport.WrapperFunc which provides the test
// recorder as an http.RoundTripper.
func transportRecorderWrapper(t *testing.T, fixtureYaml string) (transport.WrapperFunc, func()) {
	t.Helper()

	rec, teardown := testRecorder(t, fixtureYaml, testingMode, nil)
	return func(r http.RoundTripper) http.RoundTripper {
		rec.SetTransport(r)
		return rec
	}, teardown
}

// getRegionsWithCaps returns a list of regions that support the given capabilities.
func getRegionsWithCaps(t *testing.T, client *linodego.Client, capabilities []string) []string {
	result := make([]string, 0)

	regions, err := client.ListRegions(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}

	regionHasCaps := func(r linodego.Region) bool {
		capsMap := make(map[string]bool)

		for _, c := range r.Capabilities {
			capsMap[strings.ToUpper(c)] = true
		}

		for _, c := range capabilities {
			if _, ok := capsMap[strings.ToUpper(c)]; !ok {
				return false
			}
		}

		return true
	}

	for _, region := range regions {
		if region.Status != "ok" || !regionHasCaps(region) {
			continue
		}

		result = append(result, region.ID)
	}

	return result
}
