package victorialogs_test

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/tomb.v2"

	"github.com/crowdsecurity/go-cs-lib/cstest"

	"github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/victorialogs"
	"github.com/crowdsecurity/crowdsec/pkg/metrics"
	"github.com/crowdsecurity/crowdsec/pkg/pipeline"
)

func TestConfiguration(t *testing.T) {
	log.Infof("Test 'TestConfigure'")

	ctx := t.Context()

	tests := []struct {
		config       string
		expectedErr  string
		password     string
		waitForReady time.Duration
		testName     string
	}{
		{
			config:      `foobar: asd`,
			expectedErr: `[1:1] unknown field "foobar"`,
			testName:    "Unknown field",
		},
		{
			config: `
mode: tail
source: victorialogs`,
			expectedErr: "url is mandatory",
			testName:    "Missing url",
		},
		{
			config: `
mode: tail
source: victorialogs
url: http://localhost:9428/
`,
			expectedErr: "query is mandatory",
			testName:    "Missing query",
		},
		{
			config: `
mode: tail
source: victorialogs
url: http://localhost:9428/
query: >
        {server="demo"}
limit: true
`,
			expectedErr: "[7:8] cannot unmarshal bool into Go struct field Configuration.Limit of type int",
			testName:    "mismatched type",
		},
		{
			config: `
mode: tail
source: victorialogs
url: http://localhost:9428/
query: >
        {server="demo"}
`,
			expectedErr: "",
			testName:    "Correct config",
		},
		{
			config: `
mode: tail
source: victorialogs
url: http://localhost:9428/
wait_for_ready: 5s
query: >
        {server="demo"}
`,
			expectedErr:  "",
			testName:     "Correct config with wait_for_ready",
			waitForReady: 5 * time.Second,
		},
		{
			config: `
mode: tail
source: victorialogs
url: http://localhost:9428/
auth:
  username: foo
  password: bar
query: >
        {server="demo"}
`,
			expectedErr: "",
			password:    "bar",
			testName:    "Correct config with password",
		},
	}
	subLogger := log.WithField("type", "victorialogs")

	for _, test := range tests {
		t.Run(test.testName, func(t *testing.T) {
			vlSource := victorialogs.Source{}
			err := vlSource.Configure(ctx, []byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
			cstest.AssertErrorContains(t, err, test.expectedErr)

			if test.password != "" {
				p := vlSource.Config.Auth.Password
				if test.password != p {
					t.Fatalf("Password mismatch : %s != %s", test.password, p)
				}
			}

			if test.waitForReady != 0 {
				if vlSource.Config.WaitForReady != test.waitForReady {
					t.Fatalf("Wrong WaitForReady %v != %v", vlSource.Config.WaitForReady, test.waitForReady)
				}
			}
		})
	}
}

func TestConfigureDSN(t *testing.T) {
	log.Infof("Test 'TestConfigureDSN'")

	ctx := t.Context()

	tests := []struct {
		name         string
		dsn          string
		expectedErr  string
		since        time.Time
		password     string
		scheme       string
		waitForReady time.Duration
	}{
		{
			name:        "Wrong scheme",
			dsn:         "wrong://",
			expectedErr: "invalid DSN wrong:// for VictoriaLogs source, must start with victorialogs://",
		},
		{
			name:        "Correct DSN",
			dsn:         `victorialogs://localhost:9428/?query={server="demo"}`,
			expectedErr: "",
		},
		{
			name:        "Empty host",
			dsn:         "victorialogs://",
			expectedErr: "empty host",
		},
		{
			name:        "Invalid DSN",
			dsn:         "victorialogs",
			expectedErr: "invalid DSN victorialogs for VictoriaLogs source, must start with victorialogs://",
		},
		{
			name:  "Bad since param",
			dsn:   `victorialogs://127.0.0.1:9428/?since=3h&query={server="demo"}`,
			since: time.Now().Add(-3 * time.Hour),
		},
		{
			name:     "Basic Auth",
			dsn:      `victorialogs://login:password@localhost:3102/?query={server="demo"}`,
			password: "password",
		},
		{
			name:         "Correct DSN",
			dsn:          `victorialogs://localhost:9428/?query={server="demo"}&wait_for_ready=5s`,
			expectedErr:  "",
			waitForReady: 5 * time.Second,
		},
		{
			name:   "SSL DSN",
			dsn:    `victorialogs://localhost:9428/?ssl=true`,
			scheme: "https",
		},
	}

	for _, test := range tests {
		subLogger := log.WithFields(log.Fields{
			"type": "victorialogs",
			"name": test.name,
		})

		t.Logf("Test : %s", test.name)

		vlSource := &victorialogs.Source{}
		err := vlSource.ConfigureByDSN(ctx, test.dsn, map[string]string{"type": "testtype"}, subLogger, "")
		cstest.AssertErrorContains(t, err, test.expectedErr)

		noDuration, _ := time.ParseDuration("0s")
		if vlSource.Config.Since != noDuration && vlSource.Config.Since.Round(time.Second) != time.Since(test.since).Round(time.Second) {
			t.Fatalf("Invalid since %v", vlSource.Config.Since)
		}

		if test.password != "" {
			p := vlSource.Config.Auth.Password
			if test.password != p {
				t.Fatalf("Password mismatch : %s != %s", test.password, p)
			}
		}

		if test.scheme != "" {
			url, err := url.Parse(vlSource.Config.URL)
			require.NoError(t, err)
			require.NotNil(t, url)
			require.Equal(t, test.scheme, url.Scheme)
		}

		if test.waitForReady != 0 {
			if vlSource.Config.WaitForReady != test.waitForReady {
				t.Fatalf("Wrong WaitForReady %v != %v", vlSource.Config.WaitForReady, test.waitForReady)
			}
		}
	}
}

// Ingestion format docs: https://docs.victoriametrics.com/victorialogs/data-ingestion/#json-stream-api
func feedVLogs(ctx context.Context, logger *log.Entry, n int, title string) error {
	bb := bytes.NewBuffer(nil)
	for i := range n {
		fmt.Fprintf(bb,
			`{ "_time": %q,"_msg":"Log line #%d %v", "server": "demo", "key": %q}
`, time.Now().Format(time.RFC3339), i, title, title)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://127.0.0.1:9428/insert/jsonline?_stream_fields=server,key", bb)
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp.Body)
		logger.Error(string(b))

		return fmt.Errorf("Bad post status %d", resp.StatusCode)
	}

	logger.Info(n, " Events sent")
	// VictoriaLogs buffers data before saving to disk
	// Default flush deadline is 2s, waiting 3s to be safe
	time.Sleep(3 * time.Second)

	return nil
}

func TestOneShotAcquisition(t *testing.T) {
	cstest.SetAWSTestEnv(t)

	ctx := t.Context()

	log.SetOutput(os.Stdout)
	log.SetLevel(log.InfoLevel)
	log.Info("Test 'TestStreamingAcquisition'")

	key := strconv.Itoa(rand.Intn(1000))
	tests := []struct {
		config string
	}{
		{
			config: fmt.Sprintf(`
mode: cat
source: victorialogs
url: http://127.0.0.1:9428
query: >
  {server=demo, key=%q}
since: 1h
`, key),
		},
	}

	for _, ts := range tests {
		logger := log.New()
		subLogger := logger.WithField("type", "victorialogs")
		vlSource := victorialogs.Source{}

		err := vlSource.Configure(ctx, []byte(ts.config), subLogger, metrics.AcquisitionMetricsLevelNone)
		if err != nil {
			t.Fatalf("Unexpected error : %s", err)
		}

		err = feedVLogs(ctx, subLogger, 20, key)
		if err != nil {
			t.Fatalf("Unexpected error : %s", err)
		}

		out := make(chan pipeline.Event)
		read := 0

		go func() {
			for {
				<-out

				read++
			}
		}()

		vlTomb := tomb.Tomb{}

		err = vlSource.OneShotAcquisition(ctx, out, &vlTomb)
		if err != nil {
			t.Fatalf("Unexpected error : %s", err)
		}

		// Some logs might be buffered
		assert.Greater(t, read, 10)
	}
}

func TestStreamingAcquisition(t *testing.T) {
	cstest.SetAWSTestEnv(t)

	ctx := t.Context()

	log.SetOutput(os.Stdout)
	log.SetLevel(log.InfoLevel)
	log.Info("Test 'TestStreamingAcquisition'")

	title := time.Now().String()
	tests := []struct {
		name          string
		config        string
		expectedErr   string
		streamErr     string
		expectedLines int
	}{
		{
			name: "Bad port",
			config: `mode: tail
source: victorialogs
url: "http://127.0.0.1:9429"
query: >
  server:"demo"`, // Wrong port
			expectedErr:   "",
			streamErr:     `VictoriaLogs is not ready`,
			expectedLines: 0,
		},
		{
			name: "ok",
			config: `mode: tail
source: victorialogs
url: "http://127.0.0.1:9428"
query: >
  server:"demo"`,
			expectedErr:   "",
			streamErr:     "",
			expectedLines: 20,
		},
	}

	for _, ts := range tests {
		t.Run(ts.name, func(t *testing.T) {
			logger := log.New()
			subLogger := logger.WithFields(log.Fields{
				"type": "victorialogs",
				"name": ts.name,
			})

			out := make(chan pipeline.Event)
			vlTomb := tomb.Tomb{}
			vlSource := victorialogs.Source{}

			err := vlSource.Configure(ctx, []byte(ts.config), subLogger, metrics.AcquisitionMetricsLevelNone)
			if err != nil {
				t.Fatalf("Unexpected error : %s", err)
			}

			err = vlSource.StreamingAcquisition(ctx, out, &vlTomb)
			cstest.AssertErrorContains(t, err, ts.streamErr)

			if ts.streamErr != "" {
				return
			}

			time.Sleep(time.Second * 2) // We need to give time to start reading from the WS

			readTomb := tomb.Tomb{}
			readCtx, cancel := context.WithTimeout(ctx, time.Second*10)
			count := 0

			readTomb.Go(func() error {
				defer cancel()

				for {
					select {
					case <-readCtx.Done():
						return readCtx.Err()
					case evt := <-out:
						count++

						if !strings.HasSuffix(evt.Line.Raw, title) {
							return fmt.Errorf("Incorrect suffix : %s", evt.Line.Raw)
						}

						if count == ts.expectedLines {
							return nil
						}
					}
				}
			})

			err = feedVLogs(ctx, subLogger, ts.expectedLines, title)
			if err != nil {
				t.Fatalf("Unexpected error : %s", err)
			}

			err = readTomb.Wait()

			cancel()

			if err != nil {
				t.Fatalf("Unexpected error : %s", err)
			}

			assert.Equal(t, ts.expectedLines, count)
		})
	}
}

func TestStopStreaming(t *testing.T) {
	cstest.SetAWSTestEnv(t)

	ctx := t.Context()

	config := `
mode: tail
source: victorialogs
url: http://127.0.0.1:9428
query: >
  server:"demo"
`
	logger := log.New()
	subLogger := logger.WithField("type", "victorialogs")
	title := time.Now().String()
	vlSource := victorialogs.Source{}

	err := vlSource.Configure(ctx, []byte(config), subLogger, metrics.AcquisitionMetricsLevelNone)
	if err != nil {
		t.Fatalf("Unexpected error : %s", err)
	}

	out := make(chan pipeline.Event, 10)

	vlTomb := &tomb.Tomb{}

	err = vlSource.StreamingAcquisition(ctx, out, vlTomb)
	if err != nil {
		t.Fatalf("Unexpected error : %s", err)
	}

	time.Sleep(time.Second * 2)

	err = feedVLogs(ctx, subLogger, 1, title)
	if err != nil {
		t.Fatalf("Unexpected error : %s", err)
	}

	vlTomb.Kill(nil)

	err = vlTomb.Wait()
	if err != nil {
		t.Fatalf("Unexpected error : %s", err)
	}
}
