//go:build linux || darwin

package terraform

import (
	"bufio"
	"bytes"
	_ "embed"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/tools/txtar"

	"github.com/coder/coder/v2/coderd/database"
	terraform_internal "github.com/coder/coder/v2/provisioner/terraform/internal"
	"github.com/coder/coder/v2/provisionersdk/proto"
)

var (
	//go:embed testdata/timings-aggregation/simple.txtar
	inputSimple []byte
	//go:embed testdata/timings-aggregation/init.txtar
	inputInit []byte
	//go:embed testdata/timings-aggregation/error.txtar
	inputError []byte
	//go:embed testdata/timings-aggregation/complete.txtar
	inputComplete []byte
	//go:embed testdata/timings-aggregation/incomplete.txtar
	inputIncomplete []byte
	//go:embed testdata/timings-aggregation/faster-than-light.txtar
	inputFasterThanLight []byte
	//go:embed testdata/timings-aggregation/multiple-resource-actions.txtar
	multipleResourceActions []byte
)

func TestAggregation(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		input []byte
	}{
		{
			name:  "init",
			input: inputInit,
		},
		{
			name:  "simple",
			input: inputSimple,
		},
		{
			name:  "error",
			input: inputError,
		},
		{
			name:  "complete",
			input: inputComplete,
		},
		{
			name:  "incomplete",
			input: inputIncomplete,
		},
		{
			name:  "faster-than-light",
			input: inputFasterThanLight,
		},
		{
			name:  "multiple-resource-actions",
			input: multipleResourceActions,
		},
	}

	// nolint:paralleltest // Not since go v1.22.
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			// txtar is a text-based archive format used in the stdlib for simple and elegant tests.
			//
			// We ALWAYS expect that the archive contains two or more "files":
			//   1. JSON logs generated by a terraform execution, one per line, *one file per stage*
			//   N. Expected resulting timings in JSON form, one per line
			arc := txtar.Parse(tc.input)
			require.GreaterOrEqual(t, len(arc.Files), 2)

			t.Logf("%s: %s", t.Name(), arc.Comment)

			var actualTimings []*proto.Timing

			// The last "file" MUST contain the expected timings.
			expectedTimings := arc.Files[len(arc.Files)-1]

			// Iterate over the initial "files" and extract their timings according to their stage.
			for i := 0; i < len(arc.Files)-1; i++ {
				file := arc.Files[i]
				stage := database.ProvisionerJobTimingStage(file.Name)
				require.Truef(t, stage.Valid(), "%q is not a valid stage name; acceptable values: %v",
					file.Name, database.AllProvisionerJobTimingStageValues())

				agg := newTimingAggregator(stage)
				ingestAllSpans(t, file.Data, agg)
				actualTimings = append(actualTimings, agg.aggregate()...)
			}

			// Ensure that the expected timings were produced.
			expected := terraform_internal.ParseTimingLines(t, expectedTimings.Data)
			terraform_internal.StableSortTimings(t, actualTimings) // To reduce flakiness.
			if !assert.True(t, terraform_internal.TimingsAreEqual(t, expected, actualTimings)) {
				t.Log("expected:")
				printTimings(t, expected)
				t.Log("actual:")
				printTimings(t, actualTimings)
			}
		})
	}
}

func ingestAllSpans(t *testing.T, input []byte, aggregator *timingAggregator) {
	t.Helper()

	scanner := bufio.NewScanner(bytes.NewBuffer(input))
	for scanner.Scan() {
		line := scanner.Bytes()
		log := parseTerraformLogLine(line)
		if log == nil {
			continue
		}

		ts, span, err := extractTimingSpan(log)
		if err != nil {
			// t.Logf("%s: failed span extraction on line: %q", err, line)
			continue
		}

		require.NotZerof(t, ts, "failed on line: %q", line)
		require.NotNilf(t, span, "failed on line: %q", line)

		aggregator.ingest(ts, span)
	}

	require.NoError(t, scanner.Err())
}

func printTimings(t *testing.T, timings []*proto.Timing) {
	t.Helper()

	for _, a := range timings {
		terraform_internal.PrintTiming(t, a)
	}
}
