package registry

import (
	"context"
	"fmt"
	"reflect"
	"strings"
	"testing"
	"time"

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

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

	now := time.Date(2026, 5, 7, 14, 0, 0, 0, time.UTC)
	queryer := &scriptedAppRegistryQueryer{
		apps: []scriptedAppRegistryRow{
			{
				id:            "app_github",
				slug:          "github",
				name:          "GitHub",
				description:   "GitHub integration",
				avatarURL:     "https://github.githubassets.com/favicons/favicon.png",
				kind:          "first_party",
				visibility:    "public",
				latestVersion: "1.0.0",
				manifestJSON:  []byte(`{"schema_version":"1","slug":"github","version":"1.0.0"}`),
				createdAt:     now,
				updatedAt:     now,
			},
			{
				id:            SearXNGAppID,
				slug:          SearXNGAppSlug,
				name:          "SearXNG",
				description:   "SearXNG integration",
				avatarURL:     "/app-icons/searxng.svg",
				kind:          "first_party",
				visibility:    "public",
				latestVersion: "1.0.0",
				manifestJSON:  []byte(`{"schema_version":"1","slug":"searxng","version":"1.0.0"}`),
				createdAt:     now,
				updatedAt:     now,
			},
		},
	}

	apps, err := New(queryer).ListApps(context.Background(), " wsp_dev ")
	if err != nil {
		t.Fatalf("ListAppsForUser returned error: %v", err)
	}
	if len(apps) != 2 {
		t.Fatalf("len(apps) = %d, want 2 (%#v)", len(apps), apps)
	}
	app := apps[0]
	if app.ID != "app_github" || app.Slug != "github" || app.Name != "GitHub" {
		t.Fatalf("unexpected app row: %#v", app)
	}
	if got, want := queryer.appArgs, []any{"wsp_dev"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("catalog args = %#v, want %#v", got, want)
	}
	if !queryer.latestManifestQueried {
		t.Fatal("catalog listing must inspect the current App manifest")
	}
	if apps[1].ID != SearXNGAppID || apps[1].Slug != SearXNGAppSlug {
		t.Fatalf("unexpected search App row: %#v", apps[1])
	}
}

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

	if !IsProductApp(EmailAppID, EmailAppSlug) {
		t.Fatal("Email exact identity must be admitted")
	}
	if !IsProductAppID(EmailAppID) || !IsProductAppSlug(EmailAppSlug) {
		t.Fatal("Email product ID and slug must be recognized")
	}
	for _, spoof := range [][2]string{
		{EmailAppID, "mail"},
		{"app_mail", EmailAppSlug},
		{EmailAppID + "_copy", EmailAppSlug},
	} {
		if IsProductApp(spoof[0], spoof[1]) {
			t.Fatalf("spoofed Email identity was admitted: %#v", spoof)
		}
	}
}

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

	now := time.Date(2026, 7, 15, 10, 0, 0, 0, time.UTC)
	validMentionAppManifest := []byte(`{
		"schema_version":"1",
		"slug":"workspace-app",
		"name":"Workspace App",
		"version":"1.0.0",
		"scopes":["app.mentions.read","messages.write"],
		"capabilities":{"runtime":{
			"runtime":{"transports":["runner_stream"],"default_transport":"runner_stream"},
			"events":["agentteam.message.mention"],
			"scopes":["app.mentions.read","messages.write"],
			"config_schema":{"type":"object"}
		}}
	}`)
	queryer := &scriptedAppRegistryQueryer{apps: []scriptedAppRegistryRow{
		{id: GitHubAppID, slug: GitHubAppSlug, name: "GitHub", kind: "first_party", visibility: "public", latestVersion: "1.0.0", manifestJSON: []byte(`{"schema_version":"1","slug":"github","version":"1.0.0"}`), createdAt: now, updatedAt: now},
		{id: SearXNGAppID, slug: SearXNGAppSlug, name: "SearXNG", kind: "first_party", visibility: "public", latestVersion: "1.0.0", manifestJSON: []byte(`{"schema_version":"1","slug":"searxng","version":"1.0.0"}`), createdAt: now, updatedAt: now},
		{id: "app_legacy_public", slug: "legacy-public", name: "Legacy Public", kind: "third_party", visibility: "public", latestVersion: "1.0.0", manifestJSON: validMentionAppManifest, createdAt: now, updatedAt: now},
		{id: GitHubAppID, slug: "github-spoof", name: "Spoofed Product ID", kind: "first_party", visibility: "public", latestVersion: "1.0.0", manifestJSON: []byte(`{}`), createdAt: now, updatedAt: now},
		{id: "app_github_spoof", slug: GitHubAppSlug, name: "Spoofed Product Slug", kind: "first_party", visibility: "public", latestVersion: "1.0.0", manifestJSON: []byte(`{}`), createdAt: now, updatedAt: now},
		{id: "app_workspace_bot", slug: "workspace-app", name: "Workspace App", kind: "third_party", visibility: "unlisted", catalogVisibility: "private", latestVersion: "1.0.0", manifestJSON: validMentionAppManifest, createdAt: now, updatedAt: now},
		{id: "app_unsupported_bot", slug: "unsupported-app", name: "Unsupported App", kind: "third_party", visibility: "unlisted", latestVersion: "1.0.0", manifestJSON: []byte(`{}`), createdAt: now, updatedAt: now},
		{id: "app_legacy_unlisted", slug: "legacy-unlisted", name: "Legacy Unlisted", kind: "third_party", visibility: "unlisted", latestVersion: "1.0.0", manifestJSON: []byte(`{"schema_version":"1","slug":"legacy-unlisted","name":"Legacy","version":"1.0.0","callback":{"url":"https://example.com/callback"}}`), createdAt: now, updatedAt: now},
	}}

	apps, err := New(queryer).ListApps(context.Background(), "wsp_catalog")
	if err != nil {
		t.Fatalf("ListApps returned error: %v", err)
	}
	if got, want := len(apps), 3; got != want {
		t.Fatalf("len(apps) = %d, want %d (%#v)", got, want, apps)
	}
	if apps[0].ID != GitHubAppID ||
		apps[1].ID != SearXNGAppID ||
		apps[2].ID != "app_workspace_bot" {
		t.Fatalf("catalog = %#v, want GitHub, SearXNG, and the supported workspace App", apps)
	}
	if apps[2].RuntimeCapability == nil || !apps[2].RuntimeCapability.Supported {
		t.Fatalf("workspace App capability = %#v, want supported", apps[2].RuntimeCapability)
	}
	if apps[2].Visibility != "unlisted" || apps[2].CatalogVisibility != "private" {
		t.Fatalf("workspace App visibilities = global %q, catalog %q", apps[2].Visibility, apps[2].CatalogVisibility)
	}
}

type scriptedAppRegistryQueryer struct {
	apps                  []scriptedAppRegistryRow
	appArgs               []any
	catalogQuery          string
	latestManifestQueried bool
}

func (queryer *scriptedAppRegistryQueryer) QueryRow(context.Context, string, ...any) coredb.Row {
	return scriptedAppRegistryRow{}
}

func (queryer *scriptedAppRegistryQueryer) Query(_ context.Context, query string, args ...any) (coredb.Rows, error) {
	queryer.latestManifestQueried = strings.Contains(query, "app.latest_version") && strings.Contains(query, "version.manifest")
	queryer.catalogQuery = query
	queryer.appArgs = append([]any(nil), args...)
	return &scriptedAppRegistryRows{rows: queryer.apps, index: -1}, nil
}

type scriptedAppRegistryRow struct {
	id                string
	slug              string
	name              string
	description       string
	avatarURL         string
	kind              string
	visibility        string
	catalogVisibility string
	createdAt         time.Time
	updatedAt         time.Time
	latestVersion     string
	manifestJSON      []byte
}

func (row scriptedAppRegistryRow) Scan(dest ...any) error {
	switch len(dest) {
	case 9:
		return scanScriptedValues(
			dest,
			row.id,
			row.slug,
			row.name,
			row.description,
			row.avatarURL,
			row.kind,
			row.visibility,
			row.createdAt,
			row.updatedAt,
		)
	case 12:
		return scanScriptedValues(
			dest,
			row.id,
			row.slug,
			row.name,
			row.description,
			row.avatarURL,
			row.kind,
			row.visibility,
			row.catalogVisibility,
			row.createdAt,
			row.updatedAt,
			row.latestVersion,
			row.manifestJSON,
		)
	default:
		return fmt.Errorf("unexpected scan destination count %d", len(dest))
	}
}

type scriptedAppRegistryRows struct {
	rows  []scriptedAppRegistryRow
	index int
}

func (rows *scriptedAppRegistryRows) Close() {}
func (rows *scriptedAppRegistryRows) Err() error {
	return nil
}
func (rows *scriptedAppRegistryRows) Next() bool {
	rows.index++
	return rows.index < len(rows.rows)
}
func (rows *scriptedAppRegistryRows) Scan(dest ...any) error {
	if rows.index < 0 || rows.index >= len(rows.rows) {
		return fmt.Errorf("scan called outside row")
	}
	return rows.rows[rows.index].Scan(dest...)
}

func scanScriptedValues(dest []any, values ...any) error {
	if len(dest) != len(values) {
		return fmt.Errorf("scan got %d destinations, want %d", len(dest), len(values))
	}
	for idx := range dest {
		target := reflect.ValueOf(dest[idx])
		if target.Kind() != reflect.Ptr || target.IsNil() {
			return fmt.Errorf("scan destination %d has type %T", idx, dest[idx])
		}
		target.Elem().Set(reflect.ValueOf(values[idx]))
	}
	return nil
}
