package analytics

import (
	"errors"
	"strings"
	"testing"
)

func TestBuildViewDDLByteLimit(t *testing.T) {
	tests := []struct {
		name string
		cfg  ViewBuildConfig
	}{
		{
			name: "single layer",
			cfg: ViewBuildConfig{
				ObjectName:    "BoundedObject",
				PhysicalTable: "bounded_object",
				TableAlias:    "t",
				BaseColumns:   []string{"Id", "Amount"},
				Formulas: []FormulaDefinition{
					{Name: "AdjustedAmount", Expression: "Amount + 1", ReturnType: "Number"},
				},
			},
		},
		{
			name: "multiple layers",
			cfg: ViewBuildConfig{
				ObjectName:    "BoundedObject",
				PhysicalTable: "bounded_object",
				TableAlias:    "t",
				BaseColumns:   []string{"Id", "Amount"},
				Formulas: []FormulaDefinition{
					{Name: "AdjustedAmount", Expression: "Amount + 1", ReturnType: "Number"},
					{Name: "FinalAmount", Expression: "AdjustedAmount + 1", ReturnType: "Number"},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			baseline, err := NewDSLViewBuilder().BuildViewDDL(tt.cfg)
			if err != nil {
				t.Fatalf("BuildViewDDL() baseline error = %v", err)
			}

			// The exact bound is inclusive and must preserve the generated bytes.
			exactBound := &DSLViewBuilder{maxViewDDLBytes: len(baseline)}
			got, err := exactBound.BuildViewDDL(tt.cfg)
			if err != nil {
				t.Fatalf("BuildViewDDL() at exact byte bound error = %v", err)
			}
			if got != baseline {
				t.Fatalf("BuildViewDDL() changed under the byte bound:\nwant:\n%s\n\ngot:\n%s", baseline, got)
			}

			maxBytes := len(baseline) - 1
			oversizedBuilder := &DSLViewBuilder{maxViewDDLBytes: maxBytes}
			ddl, err := oversizedBuilder.BuildViewDDL(tt.cfg)
			if err == nil {
				t.Fatal("BuildViewDDL() error = nil, want output-size failure")
			}
			if ddl != "" {
				t.Fatalf("BuildViewDDL() returned partial DDL on failure: %q", ddl)
			}
			if !errors.Is(err, ErrFormulaSQLOutputTooLarge) {
				t.Fatalf("BuildViewDDL() error = %v, want ErrFormulaSQLOutputTooLarge", err)
			}

			var sizeErr *FormulaSQLOutputTooLargeError
			if !errors.As(err, &sizeErr) {
				t.Fatalf("BuildViewDDL() error type = %T, want *FormulaSQLOutputTooLargeError", err)
			}
			if sizeErr.Scope != "v_boundedobject" {
				t.Fatalf("size error scope = %q, want v_boundedobject", sizeErr.Scope)
			}
			if sizeErr.ActualBytes <= maxBytes || sizeErr.MaxBytes != maxBytes {
				t.Fatalf("size error bytes = actual %d, max %d; want actual > %d and max %d",
					sizeErr.ActualBytes, sizeErr.MaxBytes, maxBytes, maxBytes)
			}
			for _, leaked := range []string{"SELECT", "Amount + 1", "COALESCE"} {
				if strings.Contains(err.Error(), leaked) {
					t.Fatalf("BuildViewDDL() error leaked SQL fragment %q: %v", leaked, err)
				}
			}
		})
	}
}

func TestBoundedSQLBuilderWriteIndentedMatchesIndent(t *testing.T) {
	for _, input := range []string{
		"SELECT 1",
		"SELECT 1\nFROM dual",
		"SELECT 1\n\nFROM dual\n",
		"",
	} {
		builder := newBoundedSQLBuilder("v_test", 1024)
		builder.WriteIndented(input, "  ")
		if err := builder.Err(); err != nil {
			t.Fatalf("WriteIndented(%q) error = %v", input, err)
		}
		if got, want := builder.String(), indent(input, "  "); got != want {
			t.Fatalf("WriteIndented(%q) = %q, want %q", input, got, want)
		}
	}
}
