package packs

import (
	"context"
	"database/sql/driver"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
)

func TestConnectedAppPackSnapshotRestoreSkipsUnchangedLiveApp(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })

	packName := "oauth-client-pack"
	appID := "synthetic-client"
	declaration := sourceBackedConnectedAppDeclarationRow{
		ID:                sourceBackedConnectedAppDeclarationID(packName, appID),
		InstalledPackName: packName,
		ConnectedAppID:    appID,
		PayloadHash:       "snapshot-hash",
		IsDeleted:         0,
	}

	mock.ExpectQuery("SELECT Id, InstalledPackName, ConnectedAppId, PayloadHash").
		WithArgs(packName).
		WillReturnRows(connectedAppSnapshotDeclarationRows().AddRow(
			declaration.ID,
			declaration.InstalledPackName,
			declaration.ConnectedAppID,
			declaration.PayloadHash,
			declaration.IsDeleted,
		))
	mock.ExpectQuery("SELECT Id, Name, ConsumerKey, ConsumerSecret").
		WithArgs(appID).
		WillReturnRows(connectedAppSnapshotAppRows().AddRow(connectedAppSnapshotAppRowValues(appID)...))

	snapshot, err := SnapshotConnectedAppPackState(context.Background(), db, packName)
	if err != nil {
		t.Fatalf("SnapshotConnectedAppPackState: %v", err)
	}

	mock.ExpectBegin()
	mock.ExpectExec("DELETE FROM _System_ConnectedAppStage").
		WithArgs(packName).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectQuery("SELECT Id, InstalledPackName, ConnectedAppId, PayloadHash").
		WithArgs(packName).
		WillReturnRows(connectedAppSnapshotDeclarationRows().AddRow(
			declaration.ID,
			declaration.InstalledPackName,
			declaration.ConnectedAppID,
			declaration.PayloadHash,
			declaration.IsDeleted,
		))
	mock.ExpectQuery("SELECT Id, Name, ConsumerKey, ConsumerSecret").
		WithArgs(appID).
		WillReturnRows(connectedAppSnapshotAppRows().AddRow(connectedAppSnapshotAppRowValues(appID)...))
	mock.ExpectCommit()

	hook := &connectedAppInvalidationHookRecorder{}
	if err := snapshot.Restore(context.Background(), ApplyDeps{DB: db, ConnectedAppHook: hook}); err != nil {
		t.Fatalf("Restore: %v", err)
	}
	if got := strings.Join(hook.appIDs, ","); got != "" {
		t.Fatalf("ConnectedAppHook appIDs = %q, want no invalidation for unchanged live app", got)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

func TestConnectedAppPackSnapshotRestoreTombstonesNewFailedInstallClient(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })

	packName := "oauth-client-pack"
	appID := "synthetic-client"
	declaration := sourceBackedConnectedAppDeclarationRow{
		ID:                sourceBackedConnectedAppDeclarationID(packName, appID),
		InstalledPackName: packName,
		ConnectedAppID:    appID,
		PayloadHash:       "failed-hash",
		IsDeleted:         0,
	}

	mock.ExpectQuery("SELECT Id, InstalledPackName, ConnectedAppId, PayloadHash").
		WithArgs(packName).
		WillReturnRows(connectedAppSnapshotDeclarationRows())

	snapshot, err := SnapshotConnectedAppPackState(context.Background(), db, packName)
	if err != nil {
		t.Fatalf("SnapshotConnectedAppPackState: %v", err)
	}

	mock.ExpectBegin()
	mock.ExpectExec("DELETE FROM _System_ConnectedAppStage").
		WithArgs(packName).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectQuery("SELECT Id, InstalledPackName, ConnectedAppId, PayloadHash").
		WithArgs(packName).
		WillReturnRows(connectedAppSnapshotDeclarationRows().AddRow(
			declaration.ID,
			declaration.InstalledPackName,
			declaration.ConnectedAppID,
			declaration.PayloadHash,
			declaration.IsDeleted,
		))
	mock.ExpectExec("UPDATE _System_ConnectedApp").
		WithArgs(appID).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE _System_ConnectedAppDeclaration").
		WithArgs(
			declaration.PayloadHash,
			1,
			declaration.ID,
			declaration.InstalledPackName,
			declaration.ConnectedAppID,
		).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()
	mock.ExpectExec("UPDATE `_System_Session` SET").
		WithArgs(appID).
		WillReturnResult(sqlmock.NewResult(0, 1))

	hook := &connectedAppInvalidationHookRecorder{}
	if err := snapshot.Restore(context.Background(), ApplyDeps{DB: db, ConnectedAppHook: hook}); err != nil {
		t.Fatalf("Restore: %v", err)
	}
	if got, want := strings.Join(hook.appIDs, ","), appID; got != want {
		t.Fatalf("ConnectedAppHook appIDs = %q, want %q", got, want)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

func connectedAppSnapshotDeclarationRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"Id",
		"InstalledPackName",
		"ConnectedAppId",
		"PayloadHash",
		"IsDeleted",
	})
}

func connectedAppSnapshotAppRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"Id",
		"Name",
		"ConsumerKey",
		"ConsumerSecret",
		"ConsumerSecretHash",
		"CallbackUrl",
		"RedirectUris",
		"Scopes",
		"IsActive",
		"ClientType",
		"RequirePkce",
		"TokenEndpointAuthMethod",
		"AllowedGrantTypes",
		"ConsentPolicy",
		"ConsentVersion",
		"IssueRefreshTokens",
		"PublisherName",
		"SupportUrl",
		"RunAsUserId",
		"PermittedUsers",
		"SessionIssuerClass",
		"DailyApiLimit",
		"HourlyApiLimit",
	})
}

func connectedAppSnapshotAppRowValues(appID string) []driver.Value {
	return []driver.Value{
		appID,
		"Synthetic Client",
		"synthetic-client-key",
		nil,
		"snapshot-secret-hash",
		"https://client.example/callback",
		`["https://client.example/callback"]`,
		"external.records.read",
		true,
		"public",
		true,
		"none",
		"authorization_code",
		"trusted",
		int64(1),
		false,
		nil,
		nil,
		nil,
		"AllUsers",
		"platform_browser",
		nil,
		nil,
	}
}
