package installation

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

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

	queryer := &scriptedInstallationQueryer{
		rows: []scriptedGrantRow{
			{installationID: "inst-a", scope: "profile.read"},
			{installationID: "inst-a", scope: " messages.write "},
			{installationID: "inst-a", scope: "messages.write"},
			{installationID: "inst-b", scope: ""},
			{installationID: "", scope: "threads.read"},
		},
	}

	grants, err := LoadActiveGrantScopesByInstallation(context.Background(), queryer, []string{" inst-b ", "", "inst-a", "inst-b"})
	if err != nil {
		t.Fatalf("LoadActiveGrantScopesByInstallation returned error: %v", err)
	}
	if got, want := queryer.args, []any{"inst-b", "inst-a"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("query args = %#v, want %#v", got, want)
	}
	if !strings.Contains(queryer.query, "WITH requested_installations AS") {
		t.Fatalf("query missing requested_installations CTE:\n%s", queryer.query)
	}
	if got, want := grants["inst-a"], []string{"messages.write", "profile.read"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("inst-a grants = %#v, want %#v", got, want)
	}
	if got, want := grants["inst-b"], []string{}; !reflect.DeepEqual(got, want) {
		t.Fatalf("inst-b grants = %#v, want %#v", got, want)
	}
}

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

	now := time.Date(2026, 6, 7, 17, 0, 0, 0, time.UTC)
	tx := &scriptedGrantSyncTx{
		activeScopes: []string{" profile.read ", "channels.read"},
		// Canonical service installation row loaded by SyncGrantScopes before
		// grant reconciliation.
		installationRow: []any{
			"wsp-1",  // workspace_id
			"def-1",  // service_definition_id
			"app",    // service_kind
			"user-1", // created_by_member_id
		},
	}

	err := SyncGrantScopes(context.Background(), tx, " inst-1 ", " user-1 ", []string{"tasks.write", "profile.read", "messages.write"}, now)
	if err != nil {
		t.Fatalf("SyncGrantScopes returned error: %v", err)
	}
	if got, want := tx.queryArgs, []any{"inst-1"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("query args = %#v, want %#v", got, want)
	}
	if got, want := len(tx.execCalls), 2; got != want {
		t.Fatalf("exec call count = %d, want %d", got, want)
	}

	revoke := tx.execCalls[0]
	if !strings.Contains(revoke.query, "scope IN ($3)") {
		t.Fatalf("revoke query was not batched:\n%s", revoke.query)
	}
	if got, want := revoke.args, []any{"inst-1", now, "channels.read"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("revoke args = %#v, want %#v", got, want)
	}

	insert := tx.execCalls[1]
	// Canonical service_grants insert carries workspace/definition/kind plus
	// scope, config, policy, timestamps and grantor per row (14 columns).
	if !strings.Contains(insert.query, "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14),") ||
		!strings.Contains(insert.query, "($15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)") {
		t.Fatalf("insert query was not a multi-row insert:\n%s", insert.query)
	}
	if got, want := len(insert.args), 28; got != want {
		t.Fatalf("insert arg count = %d, want %d", got, want)
	}
	// Row 1: [id, workspace_id, service_definition_id, service_installation_id,
	// service_kind, scope, config, policy_version, policy_digest,
	// granted_by_member_id, granted_at, expires_at, created_at, updated_at]
	if got, want := insert.args[3], any("inst-1"); got != want {
		t.Fatalf("first insert installation arg = %#v, want %#v", got, want)
	}
	if got, want := insert.args[5], any("messages.write"); got != want {
		t.Fatalf("first insert scope arg = %#v, want %#v", got, want)
	}
	if got, want := insert.args[9], any("user-1"); got != want {
		t.Fatalf("first insert grant user arg = %#v, want %#v", got, want)
	}
	// Row 2.
	if got, want := insert.args[17], any("inst-1"); got != want {
		t.Fatalf("second insert installation arg = %#v, want %#v", got, want)
	}
	if got, want := insert.args[19], any("tasks.write"); got != want {
		t.Fatalf("second insert scope arg = %#v, want %#v", got, want)
	}
	if got, want := insert.args[23], any("user-1"); got != want {
		t.Fatalf("second insert grant user arg = %#v, want %#v", got, want)
	}
}

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

	queryer := &scriptedInstallationQueryer{
		row: scriptedRow{values: []any{
			"1.2.3",
			[]byte(`{"scopes":["profile.read","messages.write","profile.read"]}`),
		}},
	}

	version, err := LoadInstallableAppVersion(context.Background(), queryer, " app_github ", " 1.2.3 ", "")
	if err != nil {
		t.Fatalf("LoadInstallableAppVersion returned error: %v", err)
	}
	if got, want := queryer.rowArgs, []any{"app_github", "1.2.3"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("query args = %#v, want %#v", got, want)
	}
	for _, unwanted := range []string{"review_status", "developer_id", "visibility"} {
		if strings.Contains(queryer.query, unwanted) {
			t.Fatalf("query unexpectedly contains %q:\n%s", unwanted, queryer.query)
		}
	}
	if version.Version != "1.2.3" {
		t.Fatalf("Version = %q, want 1.2.3", version.Version)
	}
	if got, want := version.Scopes, []string{"messages.write", "profile.read"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Scopes = %#v, want %#v", got, want)
	}
}

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

	queryer := &scriptedInstallationQueryer{
		row: scriptedRow{values: []any{
			"1.0.0",
			[]byte(`{
                "callback":{"url":"https://app.example.test/agentteam/callback"},
                "capabilities":{"runtime":{
                    "runtime":{"transports":["runner_stream","callback"],"default_transport":"runner_stream"},
                    "events":["agentteam.message.mention"],
                    "scopes":["app.mentions.read","messages.write"],
                    "config_schema":{"type":"object"}
                }}
            }`),
		}},
	}

	version, err := LoadInstallableAppVersion(context.Background(), queryer, "app_bot", "1.0.0", "")
	if err != nil {
		t.Fatalf("LoadInstallableAppVersion returned error: %v", err)
	}
	if !version.RuntimeCapabilitySupported {
		t.Fatal("RuntimeCapabilitySupported = false, want true")
	}
	if got, want := version.AppRuntimeTransports, []string{"runner_stream", "callback"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("AppRuntimeTransports = %#v, want %#v", got, want)
	}
	if got, want := version.AppCallbackURL, "https://app.example.test/agentteam/callback"; got != want {
		t.Fatalf("AppCallbackURL = %q, want %q", got, want)
	}
}

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

	for name, values := range map[string][]any{
		"legacy custom":      {"app_legacy", "legacy", "Legacy", "public"},
		"product id spoof":   {"app_github", "github-copy", "GitHub Copy", "public"},
		"product slug spoof": {"app_github_copy", "github", "GitHub Copy", "public"},
	} {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			_, err := LoadInstallableApp(context.Background(), &scriptedInstallationQueryer{row: scriptedRow{values: values}}, values[0].(string), "")
			if !errors.Is(err, ErrNotFound) {
				t.Fatalf("LoadInstallableApp err = %v, want ErrNotFound", err)
			}
		})
	}
}

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

	for name, values := range map[string][]any{
		"github":       {"app_github", "github", "GitHub", "public"},
		"gitea":        {"app_gitea", "gitea", "Gitea", "public"},
		"email":        {"app_email", "email", "Email", "public"},
		"unlisted App": {"app_bot", "workspace-app", "Workspace App", "unlisted"},
	} {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			queryer := &scriptedInstallationQueryer{
				row:  scriptedRow{values: values},
				rows: []scriptedGrantRow{},
			}
			app, err := LoadInstallableApp(context.Background(), queryer, values[0].(string), "")
			if err != nil {
				t.Fatalf("LoadInstallableApp returned error: %v", err)
			}
			if app.ID != values[0].(string) {
				t.Fatalf("app.ID = %q, want %q", app.ID, values[0])
			}
		})
	}
}

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

	_, err := LoadInstallableAppVersion(context.Background(), &scriptedInstallationQueryer{row: scriptedRow{err: sql.ErrNoRows}}, "app_github", "1.0.0", "")
	if !errors.Is(err, ErrBadRequest) {
		t.Fatalf("err = %v, want ErrBadRequest", err)
	}
	if got, want := err.Error(), "App version is not installable."; got != want {
		t.Fatalf("error = %q, want %q", got, want)
	}
}

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

	queryer := &scriptedInstallationQueryer{row: scriptedRow{err: sql.ErrNoRows}}
	_, found, err := LoadAppInstallationByAppForUpdate(context.Background(), queryer, "wsp-1", "app-1")
	if err != nil || found {
		t.Fatalf("LoadAppInstallationByAppForUpdate found=%v err=%v, want found=false err=nil", found, err)
	}
	if got, want := queryer.rowArgs, []any{"wsp-1", "app-1", "default"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("LoadAppInstallationByAppForUpdate args=%#v, want %#v", got, want)
	}
	if !strings.Contains(queryer.query, "FROM service_installations si") ||
		!strings.Contains(queryer.query, "si.instance_key = $3") ||
		!strings.Contains(queryer.query, "si.service_kind = 'app'") {
		t.Fatalf("singleton App lookup must bind the default instance key:\n%s", queryer.query)
	}

	_, err = LoadAppInstallationForUpdate(context.Background(), queryer, "wsp-1", "appinst-1")
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("LoadAppInstallationForUpdate err = %v, want ErrNotFound", err)
	}
}

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

	now := time.Date(2026, 8, 18, 12, 0, 0, 0, time.UTC)
	queryer := &scriptedInstallationQueryer{
		row: scriptedRow{values: []any{
			"appinst-mail-secondary",
			"app_email",
			"email",
			"Email",
			"wsp-1",
			"secondary",
			"mbr-mail-secondary",
			"mail-secondary",
			"Secondary Mailbox",
			"1.0.0",
			[]byte(`{"mailbox":"secondary"}`),
			true,
			"mbr-admin",
			now,
			now,
		}},
	}

	installation, found, err := LoadAppInstallationByAppInstanceForUpdate(
		context.Background(),
		queryer,
		" wsp-1 ",
		" app_email ",
		" secondary ",
	)
	if err != nil || !found {
		t.Fatalf("LoadAppInstallationByAppInstanceForUpdate found=%v err=%v", found, err)
	}
	if got, want := queryer.rowArgs, []any{"wsp-1", "app_email", "secondary"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("query args = %#v, want %#v", got, want)
	}
	if !strings.Contains(queryer.query, "si.service_kind = 'app'") {
		t.Fatalf("named App lookup must reject non-App service installations:\n%s", queryer.query)
	}
	if installation.InstanceKey != "secondary" ||
		installation.MemberDisplayName != "Secondary Mailbox" {
		t.Fatalf("installation identity = %#v", installation)
	}
}

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

	_, err := MarshalJSONObject(map[string]any{"bad": make(chan int)}, "config")
	if !errors.Is(err, ErrBadRequest) {
		t.Fatalf("err = %v, want ErrBadRequest", err)
	}
	if got, want := err.Error(), "Invalid config object."; got != want {
		t.Fatalf("error = %q, want %q", got, want)
	}
}

type scriptedInstallationQueryer struct {
	row     scriptedRow
	rowArgs []any
	rows    []scriptedGrantRow
	query   string
	args    []any
}

func (queryer *scriptedInstallationQueryer) QueryRow(_ context.Context, query string, args ...any) coredb.Row {
	queryer.query = query
	queryer.rowArgs = append([]any(nil), args...)
	return queryer.row
}

func (queryer *scriptedInstallationQueryer) Query(_ context.Context, query string, args ...any) (coredb.Rows, error) {
	queryer.query = query
	queryer.args = append([]any(nil), args...)
	return &scriptedGrantRows{rows: queryer.rows}, nil
}

type scriptedRow struct {
	values []any
	err    error
}

func (row scriptedRow) Scan(dest ...any) error {
	if row.err != nil {
		return row.err
	}
	for idx, value := range row.values {
		switch target := dest[idx].(type) {
		case *string:
			*target = value.(string)
		case *[]byte:
			*target = append((*target)[:0], value.([]byte)...)
		case *bool:
			*target = value.(bool)
		case *time.Time:
			*target = value.(time.Time)
		default:
			panic("unsupported scan target")
		}
	}
	return nil
}

type scriptedGrantRow struct {
	installationID string
	scope          string
}

type scriptedGrantRows struct {
	rows []scriptedGrantRow
	idx  int
}

func (rows *scriptedGrantRows) Close()     {}
func (rows *scriptedGrantRows) Err() error { return nil }
func (rows *scriptedGrantRows) Next() bool { return rows.idx < len(rows.rows) }
func (rows *scriptedGrantRows) Scan(dest ...any) error {
	row := rows.rows[rows.idx]
	rows.idx++
	*(dest[0].(*string)) = row.installationID
	*(dest[1].(*string)) = row.scope
	return nil
}

type scriptedGrantSyncTx struct {
	activeScopes    []string
	query           string
	queryArgs       []any
	execCalls       []scriptedGrantExecCall
	installationRow []any
}

func (tx *scriptedGrantSyncTx) QueryRow(_ context.Context, query string, args ...any) coredb.Row {
	// The resolved grant-sync path loads the service installation row first.
	// Return a valid resolved installation so the grant reconciliation
	// proceeds past the load step.
	if tx.installationRow != nil {
		return scriptedRow{values: tx.installationRow}
	}
	return scriptedRow{err: sql.ErrNoRows}
}

func (tx *scriptedGrantSyncTx) Query(_ context.Context, query string, args ...any) (coredb.Rows, error) {
	tx.query = query
	tx.queryArgs = append([]any(nil), args...)
	return &scriptedActiveGrantRows{scopes: tx.activeScopes}, nil
}

func (tx *scriptedGrantSyncTx) Exec(_ context.Context, query string, args ...any) (coredb.Result, error) {
	tx.execCalls = append(tx.execCalls, scriptedGrantExecCall{
		query: query,
		args:  append([]any(nil), args...),
	})
	return scriptedGrantResult(1), nil
}

func (tx *scriptedGrantSyncTx) Commit(context.Context) error {
	return nil
}

func (tx *scriptedGrantSyncTx) Rollback(context.Context) error {
	return nil
}

type scriptedGrantExecCall struct {
	query string
	args  []any
}

type scriptedGrantResult int64

func (result scriptedGrantResult) RowsAffected() int64 {
	return int64(result)
}

type scriptedActiveGrantRows struct {
	scopes []string
	idx    int
}

func (rows *scriptedActiveGrantRows) Close()     {}
func (rows *scriptedActiveGrantRows) Err() error { return nil }
func (rows *scriptedActiveGrantRows) Next() bool { return rows.idx < len(rows.scopes) }
func (rows *scriptedActiveGrantRows) Scan(dest ...any) error {
	scope := rows.scopes[rows.idx]
	rows.idx++
	*(dest[0].(*string)) = scope
	return nil
}
