package server

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	contentstore "github.com/tidbcloud/AgentTeam/internal/store/content"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type fixedGraphQLMutationResolver struct {
	repositories []string
	mutation     bool
	err          error
}

func (resolver fixedGraphQLMutationResolver) ResolveGraphQLMutationRepositories(context.Context, []byte) ([]string, bool, error) {
	return append([]string(nil), resolver.repositories...), resolver.mutation, resolver.err
}

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

	database := newWorkspaceAGSMutationTestDB(t)
	repositories := repos.New(database)
	content := contentstore.New(database)
	policy := newWorkspaceAGSRepositoryAuthority(database)
	seedWorkspaceAGSAutomationBinding(t, repositories, "wsp_1", "Acme/Docs")

	allowed, err := policy.AllowLegacyRepositoryMutation(t.Context(), "wsp_1", "acme/docs")
	require.NoError(t, err)
	require.True(t, allowed)

	_, err = content.ActivateNativeAuthority(t.Context(), contentstore.ActivateNativeAuthorityParams{
		WorkspaceID: "wsp_1", Domain: "automation", BindingKey: "Acme/Docs", ExpectedGeneration: 0,
		Checkpoint: "automation-cutover", Actor: "usr_1", Now: time.Now().UTC(),
	})
	require.NoError(t, err)

	allowed, err = policy.AllowLegacyRepositoryMutation(t.Context(), "wsp_1", "acme/docs")
	require.NoError(t, err)
	require.False(t, allowed)

	otherWorkspace, err := policy.AllowLegacyRepositoryMutation(t.Context(), "wsp_other", "acme/docs")
	require.NoError(t, err)
	require.True(t, otherWorkspace)
}

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

	database := newWorkspaceAGSMutationTestDB(t)
	repositories := repos.New(database)
	policy := newWorkspaceAGSRepositoryAuthority(database)
	now := time.Now().UTC()
	_, err := repositories.Upsert(t.Context(), repos.ResourceRepository{
		ID: "rr_skill", WorkspaceID: "wsp_1", ResourceType: repos.ResourceTypeSkillRepository,
		ResourceID: "sk_1", RepoRole: repos.RepoRolePrimary, OwnerKind: "workspace",
		RepoOwner: "acme", RepoName: "skills", RepoFullName: "acme/skills", CreatedBy: "usr_1", UpdatedBy: "usr_1",
		CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)

	bound, err := policy.IsBoundToWorkspace(t.Context(), "wsp_1", "acme/skills")
	require.NoError(t, err)
	require.True(t, bound)
	allowed, err := policy.AllowLegacyRepositoryMutation(t.Context(), "wsp_1", "acme/skills")
	require.NoError(t, err)
	require.True(t, allowed)
}

func TestWorkspaceAGSRepositoryAuthorityUsesTwoExactQueries(t *testing.T) {
	database := newWorkspaceAGSMutationTestDB(t)
	repositories := repos.New(database)
	policy := newWorkspaceAGSRepositoryAuthority(database)
	seedWorkspaceAGSAutomationBinding(t, repositories, "wsp_1", "acme/docs")

	queries := 0
	callbackName := "test:workspace_ags_mutation_query_count"
	require.NoError(t, database.Callback().Query().Before("gorm:query").Register(callbackName, func(*gorm.DB) {
		queries++
	}))
	t.Cleanup(func() { _ = database.Callback().Query().Remove(callbackName) })

	allowed, err := policy.AllowLegacyRepositoryMutation(context.Background(), "wsp_1", "acme/docs")
	require.NoError(t, err)
	require.True(t, allowed)
	require.Equal(t, 2, queries, "one exact resource binding and one exact authority lookup")
}

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

	database := newWorkspaceAGSMutationTestDB(t)
	repositories := repos.New(database)
	policy := newWorkspaceAGSRepositoryAuthority(database)
	seedWorkspaceAGSAutomationBinding(t, repositories, "wsp_1", "other/docs")

	// Reuse the request-derived resource ID with a different persisted name to
	// prove that a hash-key lookup alone never authorizes the mutation.
	resourceID := repos.AutomationRepositoryResourceID("acme/docs")
	now := time.Now().UTC()
	_, err := repositories.Upsert(t.Context(), repos.ResourceRepository{
		ID:          repos.ResourceRepositoryBindingID("wsp_1", repos.ResourceTypeAutomationRepository, resourceID, repos.RepoRolePrimary),
		WorkspaceID: "wsp_1", ResourceType: repos.ResourceTypeAutomationRepository, ResourceID: resourceID,
		RepoRole: repos.RepoRolePrimary, OwnerKind: "automation_repository",
		RepoOwner: "other", RepoName: "docs", RepoFullName: "other/docs", CreatedBy: "usr_1", UpdatedBy: "usr_1",
		CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)

	allowed, err := policy.AllowLegacyRepositoryMutation(t.Context(), "wsp_1", "acme/docs")
	require.ErrorContains(t, err, "does not match canonical repository")
	require.False(t, allowed)
}

func TestLegacyRepositoryMutationCutoffFailsClosedWhenStoreIsUnavailable(t *testing.T) {
	database := newWorkspaceAGSMutationTestDB(t)
	sqlDatabase, err := database.DB()
	require.NoError(t, err)
	require.NoError(t, sqlDatabase.Close())

	nextCalls := 0
	handler := legacyRepositoryMutationCutoff(
		http.HandlerFunc(func(http.ResponseWriter, *http.Request) { nextCalls++ }),
		newWorkspaceAGSRepositoryAuthority(database),
		nil,
	)
	request := httptest.NewRequest(http.MethodPut, "/api/v3/repos/acme/native/contents/status.json", nil)
	recorder := httptest.NewRecorder()
	handler.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusServiceUnavailable, recorder.Code, recorder.Body.String())
	require.Zero(t, nextCalls)
}

func TestLegacyRepositoryMutationCutoffRejectsExactNativeGraphQLTarget(t *testing.T) {
	database := newWorkspaceAGSMutationTestDB(t)
	repositories := repos.New(database)
	content := contentstore.New(database)
	seedWorkspaceAGSAutomationBinding(t, repositories, "wsp_1", "acme/native")
	_, err := content.ActivateNativeAuthority(t.Context(), contentstore.ActivateNativeAuthorityParams{
		WorkspaceID: "wsp_1", Domain: "automation", BindingKey: "acme/native", ExpectedGeneration: 0,
		Checkpoint: "automation-cutover", Actor: "usr_1", Now: time.Now().UTC(),
	})
	require.NoError(t, err)

	nextCalls := 0
	handler := legacyRepositoryMutationCutoff(
		http.HandlerFunc(func(http.ResponseWriter, *http.Request) { nextCalls++ }),
		newWorkspaceAGSRepositoryAuthority(database),
		fixedGraphQLMutationResolver{repositories: []string{"acme/native"}, mutation: true},
	)
	request := httptest.NewRequest(http.MethodPost, "/api/graphql", bytes.NewBufferString(`{"query":"mutation { staleWrite }"}`))
	recorder := httptest.NewRecorder()
	handler.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusConflict, recorder.Code, recorder.Body.String())
	require.Zero(t, nextCalls)
}

func TestLegacyRepositoryMutationCutoffPreservesGraphQLQueryAndLegacyBody(t *testing.T) {
	database := newWorkspaceAGSMutationTestDB(t)
	repositories := repos.New(database)
	seedWorkspaceAGSAutomationBinding(t, repositories, "wsp_1", "acme/legacy")
	body := []byte(`{"query":"mutation { legacyWrite }"}`)

	nextCalls := 0
	handler := legacyRepositoryMutationCutoff(
		http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
			nextCalls++
			forwarded, err := io.ReadAll(request.Body)
			require.NoError(t, err)
			require.Equal(t, body, forwarded)
			writer.WriteHeader(http.StatusNoContent)
		}),
		newWorkspaceAGSRepositoryAuthority(database),
		fixedGraphQLMutationResolver{repositories: []string{"acme/legacy"}, mutation: true},
	)
	request := httptest.NewRequest(http.MethodPost, "/api/graphql", bytes.NewReader(body))
	recorder := httptest.NewRecorder()
	handler.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusNoContent, recorder.Code, recorder.Body.String())
	require.Equal(t, 1, nextCalls)

	queryCalls := 0
	queryHandler := legacyRepositoryMutationCutoff(
		http.HandlerFunc(func(http.ResponseWriter, *http.Request) { queryCalls++ }),
		newWorkspaceAGSRepositoryAuthority(database),
		fixedGraphQLMutationResolver{mutation: false},
	)
	queryHandler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/api/graphql", bytes.NewBufferString(`{"query":"query { viewer { login } }"}`)))
	require.Equal(t, 1, queryCalls)
}

func newWorkspaceAGSMutationTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.NewReplacer("/", "_", " ", "_").Replace(t.Name()))
	database, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)
	sqlDatabase, err := database.DB()
	require.NoError(t, err)
	t.Cleanup(func() {
		if err := sqlDatabase.Close(); err != nil {
			t.Errorf("close workspace AGS mutation test database: %v", err)
		}
	})
	require.NoError(t, database.AutoMigrate(&repos.ResourceRepository{}, &contentstore.Authority{}))
	return database
}

func seedWorkspaceAGSAutomationBinding(t *testing.T, repositories *repos.Store, workspaceID, repository string) {
	t.Helper()
	owner, name, ok := strings.Cut(repository, "/")
	require.True(t, ok)
	resourceID := repos.AutomationRepositoryResourceID(repository)
	now := time.Now().UTC()
	_, err := repositories.Upsert(t.Context(), repos.ResourceRepository{
		ID:          repos.ResourceRepositoryBindingID(workspaceID, repos.ResourceTypeAutomationRepository, resourceID, repos.RepoRolePrimary),
		WorkspaceID: workspaceID, ResourceType: repos.ResourceTypeAutomationRepository, ResourceID: resourceID,
		RepoRole: repos.RepoRolePrimary, OwnerKind: "automation_repository",
		RepoOwner: owner, RepoName: name, RepoFullName: repository, CreatedBy: "usr_1", UpdatedBy: "usr_1",
		CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
}
