package connectorbindingdefinitionauthority

import (
	"context"
	"database/sql"
	"errors"
	"reflect"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	consumerv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	connectorbindingownerv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/connectorbindingownerv1"
)

func TestReadVerifiedDefinitionSetRequiresTransaction(t *testing.T) {
	coordinate, _ := definitionSetForTest(t, false)
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()

	_, err = ReadVerifiedDefinitionSet(context.Background(), db, coordinate)
	if !errors.Is(err, ErrPrecondition) || !strings.Contains(err.Error(), "one SQL transaction") {
		t.Fatalf("non-transactional read error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadVerifiedDefinitionSetAcceptsCompleteAndEmptySets(t *testing.T) {
	for _, withEntry := range []bool{false, true} {
		name := "empty"
		if withEntry {
			name = "complete"
		}
		t.Run(name, func(t *testing.T) {
			coordinate, set := definitionSetForTest(t, withEntry)
			db, mock, tx := definitionAuthorityTestTransaction(t)
			defer db.Close()
			expectDefinitionSetRead(mock, coordinate, set, "")

			actual, err := ReadVerifiedDefinitionSet(context.Background(), tx, coordinate)
			if err != nil {
				t.Fatalf("read verified definition set: %v", err)
			}
			if !reflect.DeepEqual(actual, set) {
				t.Fatalf("definition set = %#v, want %#v", actual, set)
			}
			finishDefinitionAuthorityTestTransaction(t, mock, tx)
		})
	}
}

func TestReadVerifiedBindingDefinitionReturnsDetachedCanonicalDefinition(t *testing.T) {
	coordinate, set := definitionSetForTest(t, true)
	db, mock, tx := definitionAuthorityTestTransaction(t)
	defer db.Close()
	expectDefinitionSetRead(mock, coordinate, set, "")
	entry := set.Entries[0]

	definition, err := ReadVerifiedBindingDefinition(
		context.Background(),
		tx,
		coordinate,
		consumerv1.IntegrationBinding{
			Kind:               entry.Kind,
			OwnerPack:          entry.OwnerPack,
			PackInstallationID: entry.PackInstallationID,
			PackPayloadDigest:  entry.PackPayloadDigest,
			BusinessKey:        entry.BusinessKey,
			DefinitionVersion:  entry.DefinitionVersion,
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	if string(definition.CanonicalDefinition) != `{"action":"push"}` {
		t.Fatalf("canonical definition = %s", definition.CanonicalDefinition)
	}
	definition.CanonicalDefinition[0] = 'x'
	stored, err := connectorbindingownerv1.ParseDefinitionEnvelopeJSON(entry.EnvelopeJSON)
	if err != nil {
		t.Fatal(err)
	}
	if string(stored.CanonicalDefinition) != `{"action":"push"}` {
		t.Fatalf("returned definition retained stored bytes: %s", stored.CanonicalDefinition)
	}
	finishDefinitionAuthorityTestTransaction(t, mock, tx)
}

func TestReadVerifiedDefinitionSetRejectsMissingHeader(t *testing.T) {
	coordinate, _ := definitionSetForTest(t, false)
	db, mock, tx := definitionAuthorityTestTransaction(t)
	defer db.Close()
	expectPackGenerationParent(mock, coordinate)
	mock.ExpectQuery(headerQueryPattern()).
		WithArgs(coordinate.StorageID, coordinate.StorageID).
		WillReturnRows(definitionSetHeaderRows())

	_, err := ReadVerifiedDefinitionSet(context.Background(), tx, coordinate)
	if !errors.Is(err, ErrConflict) || !strings.Contains(err.Error(), "missing") {
		t.Fatalf("missing header error = %v", err)
	}
	finishDefinitionAuthorityTestTransaction(t, mock, tx)
}

func TestReadVerifiedDefinitionSetRejectsMismatchedPackGenerationParent(t *testing.T) {
	coordinate, _ := definitionSetForTest(t, false)
	tests := []struct {
		name   string
		stored *GenerationCoordinate
	}{
		{name: "missing"},
		{
			name: "storage ID",
			stored: &GenerationCoordinate{
				StorageID:          strings.ToUpper(coordinate.StorageID),
				OwnerPack:          coordinate.OwnerPack,
				PackInstallationID: coordinate.PackInstallationID,
				PackPayloadDigest:  coordinate.PackPayloadDigest,
			},
		},
		{
			name: "owner pack",
			stored: &GenerationCoordinate{
				StorageID:          coordinate.StorageID,
				OwnerPack:          "other-pack",
				PackInstallationID: coordinate.PackInstallationID,
				PackPayloadDigest:  coordinate.PackPayloadDigest,
			},
		},
		{
			name: "pack installation",
			stored: &GenerationCoordinate{
				StorageID:          coordinate.StorageID,
				OwnerPack:          coordinate.OwnerPack,
				PackInstallationID: "pack-install-2",
				PackPayloadDigest:  coordinate.PackPayloadDigest,
			},
		},
		{
			name: "payload digest",
			stored: &GenerationCoordinate{
				StorageID:          coordinate.StorageID,
				OwnerPack:          coordinate.OwnerPack,
				PackInstallationID: coordinate.PackInstallationID,
				PackPayloadDigest:  "nexus-pack-sha256-v1:" + strings.Repeat("b", 64),
			},
		},
		{
			name: "null payload digest",
			stored: &GenerationCoordinate{
				StorageID:          coordinate.StorageID,
				OwnerPack:          coordinate.OwnerPack,
				PackInstallationID: coordinate.PackInstallationID,
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			db, mock, tx := definitionAuthorityTestTransaction(t)
			defer db.Close()
			rows := packGenerationParentRows()
			if test.stored != nil {
				var payload any = test.stored.PackPayloadDigest
				if test.name == "null payload digest" {
					payload = nil
				}
				rows.AddRow(
					test.stored.StorageID,
					test.stored.OwnerPack,
					test.stored.PackInstallationID,
					payload,
				)
			}
			mock.ExpectQuery(packGenerationParentQueryPattern()).
				WithArgs(coordinate.StorageID, coordinate.StorageID).
				WillReturnRows(rows)

			_, err := ReadVerifiedDefinitionSet(context.Background(), tx, coordinate)
			if !errors.Is(err, ErrConflict) ||
				(test.stored == nil && !strings.Contains(err.Error(), "missing")) ||
				(test.stored != nil && !strings.Contains(err.Error(), "exact storage coordinate")) {
				t.Fatalf("parent mismatch error = %v", err)
			}
			finishDefinitionAuthorityTestTransaction(t, mock, tx)
		})
	}
}

func TestReadVerifiedBindingRejectsMismatchedPackGenerationParent(t *testing.T) {
	coordinate, set := definitionSetForTest(t, true)
	db, mock, tx := definitionAuthorityTestTransaction(t)
	defer db.Close()
	mock.ExpectQuery(packGenerationParentQueryPattern()).
		WithArgs(coordinate.StorageID, coordinate.StorageID).
		WillReturnRows(packGenerationParentRows().AddRow(
			coordinate.StorageID,
			coordinate.OwnerPack,
			coordinate.PackInstallationID,
			"nexus-pack-sha256-v1:"+strings.Repeat("b", 64),
		))
	entry := set.Entries[0]
	err := ReadVerifiedBinding(context.Background(), tx, coordinate, consumerv1.IntegrationBinding{
		Kind:               entry.Kind,
		OwnerPack:          entry.OwnerPack,
		PackInstallationID: entry.PackInstallationID,
		PackPayloadDigest:  entry.PackPayloadDigest,
		BusinessKey:        entry.BusinessKey,
		DefinitionVersion:  entry.DefinitionVersion,
	})
	if !errors.Is(err, ErrConflict) || !strings.Contains(err.Error(), "exact storage coordinate") {
		t.Fatalf("binding parent mismatch error = %v", err)
	}
	finishDefinitionAuthorityTestTransaction(t, mock, tx)
}

func TestReadVerifiedDefinitionSetRejectsByteDistinctHiddenChild(t *testing.T) {
	coordinate, emptySet := definitionSetForTest(t, false)
	_, populatedSet := definitionSetForTest(t, true)
	entry := populatedSet.Entries[0]
	for _, childGenerationID := range []string{
		strings.ToUpper(coordinate.StorageID),
		coordinate.StorageID + " ",
	} {
		name := "case"
		if strings.HasSuffix(childGenerationID, " ") {
			name = "trailing-space"
		}
		t.Run(name, func(t *testing.T) {
			db, mock, tx := definitionAuthorityTestTransaction(t)
			defer db.Close()
			expectPackGenerationParent(mock, coordinate)
			expectDefinitionSetHeader(mock, coordinate, emptySet)
			expectDefinitionRows(mock, coordinate, []connectorbindingownerv1.DefinitionSetEntry{entry}, childGenerationID)

			_, err := ReadVerifiedDefinitionSet(context.Background(), tx, coordinate)
			if !errors.Is(err, ErrConflict) || !strings.Contains(err.Error(), "different generation") {
				t.Fatalf("hidden child error = %v", err)
			}
			finishDefinitionAuthorityTestTransaction(t, mock, tx)
		})
	}
}

func TestReadVerifiedBindingRejectsAbsentBinding(t *testing.T) {
	coordinate, set := definitionSetForTest(t, true)
	db, mock, tx := definitionAuthorityTestTransaction(t)
	defer db.Close()
	expectDefinitionSetRead(mock, coordinate, set, "")

	err := ReadVerifiedBinding(context.Background(), tx, coordinate, consumerv1.IntegrationBinding{
		Kind:               consumerv1.BindingConnectorPushContract,
		OwnerPack:          coordinate.OwnerPack,
		PackInstallationID: coordinate.PackInstallationID,
		PackPayloadDigest:  coordinate.PackPayloadDigest,
		BusinessKey:        "owner-pack/absent",
		DefinitionVersion:  1,
	})
	if !errors.Is(err, ErrConflict) || !strings.Contains(err.Error(), "absent") {
		t.Fatalf("absent binding error = %v", err)
	}
	finishDefinitionAuthorityTestTransaction(t, mock, tx)
}

func definitionSetForTest(
	t *testing.T,
	withEntry bool,
) (GenerationCoordinate, connectorbindingownerv1.DefinitionSet) {
	t.Helper()
	coordinate := GenerationCoordinate{
		OwnerPack:          "owner-pack",
		PackInstallationID: "pack-install-1",
		PackPayloadDigest:  "nexus-pack-sha256-v1:" + strings.Repeat("a", 64),
	}
	coordinate.StorageID = PackGenerationStorageID(
		coordinate.OwnerPack,
		coordinate.PackInstallationID,
	)
	definitions := []connectorbindingownerv1.DefinitionEnvelope(nil)
	if withEntry {
		definition, err := connectorbindingownerv1.NewDefinitionEnvelope(
			connectorbindingownerv1.AuthoringInput{
				Identity: connectorbindingownerv1.Identity{
					Kind:               consumerv1.BindingConnectorPushContract,
					OwnerPack:          coordinate.OwnerPack,
					PackInstallationID: coordinate.PackInstallationID,
					PackPayloadDigest:  coordinate.PackPayloadDigest,
					BusinessKey:        "owner-pack/present",
					DefinitionVersion:  1,
				},
				CanonicalDefinition: []byte(`{"action":"push"}`),
			},
		)
		if err != nil {
			t.Fatal(err)
		}
		definitions = append(definitions, definition)
	}
	set, err := connectorbindingownerv1.NewDefinitionSet(
		connectorbindingownerv1.DefinitionSetCoordinate{
			OwnerPack:          coordinate.OwnerPack,
			PackInstallationID: coordinate.PackInstallationID,
			PackPayloadDigest:  coordinate.PackPayloadDigest,
		},
		definitions,
	)
	if err != nil {
		t.Fatal(err)
	}
	return coordinate, set
}

func expectDefinitionSetRead(
	mock sqlmock.Sqlmock,
	coordinate GenerationCoordinate,
	set connectorbindingownerv1.DefinitionSet,
	childGenerationID string,
) {
	expectPackGenerationParent(mock, coordinate)
	expectDefinitionSetHeader(mock, coordinate, set)
	expectDefinitionRows(mock, coordinate, set.Entries, childGenerationID)
}

func expectPackGenerationParent(
	mock sqlmock.Sqlmock,
	coordinate GenerationCoordinate,
) {
	mock.ExpectQuery(packGenerationParentQueryPattern()).
		WithArgs(coordinate.StorageID, coordinate.StorageID).
		WillReturnRows(packGenerationParentRows().AddRow(
			coordinate.StorageID,
			coordinate.OwnerPack,
			coordinate.PackInstallationID,
			coordinate.PackPayloadDigest,
		))
}

func expectDefinitionSetHeader(
	mock sqlmock.Sqlmock,
	coordinate GenerationCoordinate,
	set connectorbindingownerv1.DefinitionSet,
) {
	mock.ExpectQuery(headerQueryPattern()).
		WithArgs(coordinate.StorageID, coordinate.StorageID).
		WillReturnRows(definitionSetHeaderRows().AddRow(
			coordinate.StorageID,
			set.Header.Protocol,
			set.Header.DefinitionCount,
			set.Header.OrderedSetDigest,
		))
}

func expectDefinitionRows(
	mock sqlmock.Sqlmock,
	coordinate GenerationCoordinate,
	entries []connectorbindingownerv1.DefinitionSetEntry,
	childGenerationID string,
) {
	rows := sqlmock.NewRows([]string{
		"Id", "PackGenerationId", "PackInstallId", "DefinitionOrdinal", "Kind",
		"OwnerPack", "PackPayloadDigest", "BusinessKey", "DefinitionVersion",
		"DefinitionEnvelopeDigest", "DefinitionEnvelope",
	})
	for _, entry := range entries {
		generationID := childGenerationID
		if generationID == "" {
			generationID = coordinate.StorageID
		}
		rows.AddRow(
			DefinitionStorageID(coordinate.StorageID, entry.Kind, entry.BusinessKey),
			generationID,
			entry.PackInstallationID,
			entry.Ordinal,
			entry.Kind,
			entry.OwnerPack,
			entry.PackPayloadDigest,
			entry.BusinessKey,
			entry.DefinitionVersion,
			entry.EnvelopeDigest,
			entry.EnvelopeJSON,
		)
	}
	mock.ExpectQuery(definitionRowsQueryPattern()).
		WithArgs(coordinate.StorageID).
		WillReturnRows(rows)
}

func definitionSetHeaderRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"PackGenerationId", "Protocol", "DefinitionCount", "DefinitionSetDigest",
	})
}

func definitionAuthorityTestTransaction(
	t *testing.T,
) (*sql.DB, sqlmock.Sqlmock, *sql.Tx) {
	t.Helper()
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	mock.ExpectBegin()
	tx, err := db.Begin()
	if err != nil {
		db.Close()
		t.Fatal(err)
	}
	return db, mock, tx
}

func finishDefinitionAuthorityTestTransaction(
	t *testing.T,
	mock sqlmock.Sqlmock,
	tx *sql.Tx,
) {
	t.Helper()
	mock.ExpectRollback()
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func packGenerationParentRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"Id", "PackName", "PackInstallId", "AppliedPayloadHash",
	})
}

func packGenerationParentQueryPattern() string {
	return `(?s)FROM _System_PackGeneration.*WHERE Id = \?.*AND BINARY Id = BINARY \?.*LIMIT 1.*FOR UPDATE`
}

func headerQueryPattern() string {
	return `(?s)FROM _System_PackGenerationConnectorBindingDefinitionSet.*WHERE PackGenerationId = \?.*AND BINARY PackGenerationId = BINARY \?.*FOR UPDATE`
}

func definitionRowsQueryPattern() string {
	return `(?s)FROM _System_PackGenerationConnectorBindingDefinition.*WHERE PackGenerationId = \?.*ORDER BY DefinitionOrdinal.*FOR UPDATE`
}
