package main

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/fieldprovenance"
)

func TestCanonicalOutputDigestExcludesFileTerminatingNewline(t *testing.T) {
	raw := []byte(`{"schema":"example/v1"}`)
	wantSum := sha256.Sum256(raw)
	want := "sha256:" + hex.EncodeToString(wantSum[:])
	if got := canonicalOutputDigest(raw); got != want {
		t.Fatalf("canonical output digest = %q, want %q", got, want)
	}
	withNewline := sha256.Sum256(append(append([]byte{}, raw...), '\n'))
	if got := canonicalOutputDigest(raw); got == "sha256:"+hex.EncodeToString(withNewline[:]) {
		t.Fatalf("canonical output digest included the file newline: %q", got)
	}
}

func TestRunDispatchesCatalogAndWorkOwnershipContractsBeforeOpeningTiDB(t *testing.T) {
	tests := []struct {
		name string
		args []string
		want string
	}{
		{name: "navigation initialization", args: []string{"application-navigation"}, want: "requires -input and -output"},
		{name: "Catalog reconcile", want: "both -config and -output are required"},
		{name: "abandon unpublished upgrade", args: []string{"abandon-unpublished-upgrade"}, want: "requires -operation-id, -evidence-digest, and -output"},
		{name: "abandon positional input", args: []string{"abandon-unpublished-upgrade", "unexpected"}, want: "accepts no positional arguments"},
		{name: "worker ownership", args: []string{"work-ownership"}, want: "both -config and -output are required"},
		{name: "worker positional input", args: []string{"work-ownership", "unexpected"}, want: "does not accept positional arguments"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := run(test.args)
			if err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("run(%q) error = %v, want %q", test.args, err, test.want)
			}
		})
	}
}

func TestPrepareCatalogRuntimeMetadataInitializesPersistedFieldProvenance(t *testing.T) {
	previous := fieldprovenance.Default()
	t.Cleanup(func() { fieldprovenance.SetDefault(previous) })
	fieldprovenance.SetDefault(nil)

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = db.Close() })

	mock.ExpectBegin()
	mock.ExpectQuery("SELECT COLUMN_NAME\\s+FROM INFORMATION_SCHEMA\\.COLUMNS").
		WithArgs(
			"_System_InstalledPack",
			"RuntimeObjectSpecDeclarations",
			"RuntimeRelatedListRules",
			"RuntimeSearchScopeOrder",
			"RuntimeSearchAllDefaultFields",
		).
		WillReturnRows(sqlmock.NewRows([]string{"COLUMN_NAME"}).
			AddRow("RuntimeObjectSpecDeclarations").
			AddRow("RuntimeRelatedListRules").
			AddRow("RuntimeSearchScopeOrder").
			AddRow("RuntimeSearchAllDefaultFields"))
	mock.ExpectQuery("SELECT COUNT\\(\\*\\)\\s+FROM INFORMATION_SCHEMA\\.COLUMNS").
		WithArgs("_System_InstalledPack", "RuntimeStartupShallowDeclarations").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	mock.ExpectQuery("SELECT COUNT\\(\\*\\)\\s+FROM INFORMATION_SCHEMA\\.COLUMNS").
		WithArgs("_System_InstalledPack", "RuntimeFormulaFunctionBundles").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	mock.ExpectQuery("SELECT Name, Category, AppliedPayloadHash, ManifestFormatVersion, SourceKind, SourceURI, SourceRevision,\\s+RuntimeObjectSpecDeclarations, RuntimeRelatedListRules, RuntimeSearchScopeOrder").
		WillReturnRows(sqlmock.NewRows([]string{
			"Name", "Category", "AppliedPayloadHash", "ManifestFormatVersion", "SourceKind", "SourceURI", "SourceRevision",
			"RuntimeObjectSpecDeclarations", "RuntimeRelatedListRules", "RuntimeSearchScopeOrder", "RuntimeSearchAllDefaultFields",
			"RuntimeFormulaFunctionBundles", "RuntimeStartupShallowDeclarations",
		}).AddRow(
			"pingcap-final", "app", "sha256:runtime-declaration", "1", nil, nil, nil,
			`[[{"apiName":"Lead","label":"Lead","tableType":"standard_object","category":"crm","sharingModel":"Private","enableHierarchySharing":false,"fields":[{"apiName":"HubSpot_Id__c","label":"HubSpot ID","type":"string"}]}]]`,
			`[]`, `[]`, `[]`, `[]`, `[]`,
		))
	mock.ExpectCommit()

	tx, err := db.BeginTx(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}
	if err := prepareCatalogRuntimeMetadata(context.Background(), tx); err != nil {
		t.Fatal(err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
	if fieldprovenance.Default() == nil {
		t.Fatal("Catalog runtime metadata preparation left field provenance uninitialized")
	}
	if got := fieldprovenance.ClassifySource("_System_Object", "Id"); got != constants.FieldSourceManaged {
		t.Fatalf("L0 provenance source = %q, want %q", got, constants.FieldSourceManaged)
	}
	if got := fieldprovenance.ClassifySource("Lead", "HubSpot_Id__c"); got != constants.FieldSourceStandardSpec {
		t.Fatalf("persisted pack provenance source = %q, want %q", got, constants.FieldSourceStandardSpec)
	}
}

func TestCatalogRuntimeMetadataPreparationIsLazyAndSnapshotBound(t *testing.T) {
	var order []string
	type transactionMarker struct{}
	prepare := func(ctx context.Context) error {
		if inside, _ := ctx.Value(transactionMarker{}).(bool); !inside {
			t.Fatal("runtime metadata preparation did not receive the transaction context")
		}
		order = append(order, "prepare")
		return nil
	}
	withinTransaction := func(ctx context.Context, fn func(context.Context) error) error {
		order = append(order, "transaction")
		return fn(context.WithValue(ctx, transactionMarker{}, true))
	}

	run := withCatalogRuntimeMetadataPreparation(prepare, withinTransaction)
	if len(order) != 0 {
		t.Fatalf("constructing the lazy transaction wrapper performed work: %v", order)
	}
	if err := run(context.Background(), func(ctx context.Context) error {
		if inside, _ := ctx.Value(transactionMarker{}).(bool); !inside {
			t.Fatal("metadata read did not receive the transaction context")
		}
		order = append(order, "read")
		return nil
	}); err != nil {
		t.Fatal(err)
	}
	want := []string{"transaction", "prepare", "read"}
	if strings.Join(order, ",") != strings.Join(want, ",") {
		t.Fatalf("lazy transaction order = %v, want %v", order, want)
	}
}

func TestCatalogRuntimeMetadataPreparationFailureStopsSnapshotRead(t *testing.T) {
	wantErr := errors.New("installed runtime declarations unavailable")
	transactions := 0
	run := withCatalogRuntimeMetadataPreparation(
		func(context.Context) error { return wantErr },
		func(ctx context.Context, read func(context.Context) error) error {
			transactions++
			return read(ctx)
		},
	)

	if err := run(context.Background(), func(context.Context) error {
		t.Fatal("metadata read ran after provenance preparation failed")
		return nil
	}); !errors.Is(err, wantErr) {
		t.Fatalf("lazy preparation error = %v, want %v", err, wantErr)
	}
	if transactions != 1 {
		t.Fatalf("opened %d metadata transactions while preparing provenance, want 1", transactions)
	}
}

func TestNavigationValidateOnlyUsesSemanticValidationWithoutDatabase(t *testing.T) {
	t.Setenv("TIDB_PORT", "not-a-port")
	valid := `{"schema":"nexuscrm.application-navigation-initialization/v1","owner_id":"crm","application_id":"app","template":[{"id":"home","label":"Home","target":{"page":{"id":"Home"}}}]}`
	for _, test := range []struct {
		name, input string
		invalid     bool
	}{
		{"valid", valid, false},
		{"empty target", strings.Replace(valid, `"id":"Home"`, `"id":""`, 1), true},
		{"unknown field", strings.Replace(valid, `"label":"Home"`, `"label":"Home","unexpected":true`, 1), true},
		{"trailing data", valid + ` {}`, true},
	} {
		t.Run(test.name, func(t *testing.T) {
			input := filepath.Join(t.TempDir(), "input.json")
			if err := os.WriteFile(input, []byte(test.input), 0600); err != nil {
				t.Fatal(err)
			}
			err := run([]string{"application-navigation", "-input", input, "-validate-only"})
			if (err != nil) != test.invalid {
				t.Fatalf("validation error = %v; invalid = %v", err, test.invalid)
			}
			if err != nil && strings.Contains(err.Error(), "not-a-port") {
				t.Fatalf("validation opened database: %v", err)
			}
		})
	}
	if err := run([]string{"application-navigation", "-input", "unused", "-validate-only", "-assert-only"}); err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
		t.Fatalf("mode conflict = %v", err)
	}
}
