package content

import (
	"bytes"
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	store "github.com/tidbcloud/AgentTeam/internal/store/content"
	"gorm.io/gorm"
)

func TestAuthorizedServiceRequiresExactScopedAction(t *testing.T) {
	core, _ := newServiceTest(t)
	ctx := context.Background()
	content, err := core.CreateContent(ctx, CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_1",
		Model: store.ModelTree, Actor: "usr_owner",
	})
	require.NoError(t, err)
	committed, err := core.CommitVersion(ctx, CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: content.ID, ExpectedGeneration: 1,
		Actor: "usr_owner", Model: store.ModelTree,
		Files: []contentpayload.File{{Path: "SKILL.md", Size: 5, Content: bytes.NewBufferString("hello")}},
	})
	require.NoError(t, err)
	authorized, err := NewAuthorizedService(core, ScopedAccessPolicy{})
	require.NoError(t, err)

	readContext := WithAccessPrincipal(ctx, AccessPrincipal{
		Subject: "usr_owner", WorkspaceID: "wsp_1", Domain: "skill",
		BindingKey: "skill_1", ContentID: content.ID,
		Actions: []AccessAction{AccessReadPayload},
	})
	paths, err := authorized.ListVersionPaths(readContext, "wsp_1", content.ID, committed.Version.ID, store.MaxCompareEntries)
	require.NoError(t, err)
	require.Len(t, paths, 1)
	require.Equal(t, committed.Version.ID, paths[0].VersionID)
	require.Equal(t, "SKILL.md", paths[0].Path)
	require.Equal(t, "file", paths[0].Kind)
	require.EqualValues(t, 5, paths[0].SizeBytes)
	body, _, err := authorized.OpenFile(readContext, "wsp_1", content.ID, committed.Version.ID, "SKILL.md", nil)
	require.NoError(t, err)
	require.NoError(t, body.Close())

	_, err = authorized.GetContent(readContext, "wsp_1", content.ID)
	require.ErrorIs(t, err, ErrAccessDenied, "payload access must not imply metadata access")

	wrongBinding := WithAccessPrincipal(ctx, AccessPrincipal{
		Subject: "usr_owner", WorkspaceID: "wsp_1", Domain: "skill",
		BindingKey: "skill_other", Actions: []AccessAction{AccessReadPayload},
	})
	_, err = authorized.ListVersionPaths(wrongBinding, "wsp_1", content.ID, committed.Version.ID, store.MaxCompareEntries)
	require.ErrorIs(t, err, ErrAccessDenied)
	_, _, err = authorized.OpenFile(wrongBinding, "wsp_1", content.ID, committed.Version.ID, "SKILL.md", nil)
	require.ErrorIs(t, err, ErrAccessDenied)

	wrongWorkspace := WithAccessPrincipal(ctx, AccessPrincipal{
		Subject: "usr_owner", WorkspaceID: "wsp_2", Domain: "skill",
		BindingKey: "skill_1", Actions: []AccessAction{AccessReadPayload},
	})
	_, err = authorized.ListVersionPaths(wrongWorkspace, "wsp_1", content.ID, committed.Version.ID, store.MaxCompareEntries)
	require.ErrorIs(t, err, ErrAccessDenied)
	_, _, err = authorized.OpenFile(wrongWorkspace, "wsp_1", content.ID, committed.Version.ID, "SKILL.md", nil)
	require.ErrorIs(t, err, ErrAccessDenied)
}

func TestAuthorizedServiceRejectsVersionFromDifferentLineageBeforePolicy(t *testing.T) {
	core, _ := newServiceTest(t)
	ctx := context.Background()
	first, err := core.CreateContent(ctx, CreateContentRequest{WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_1", Model: store.ModelTree, Actor: "usr_1"})
	require.NoError(t, err)
	second, err := core.CreateContent(ctx, CreateContentRequest{WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_2", Model: store.ModelTree, Actor: "usr_1"})
	require.NoError(t, err)
	secondVersion, err := core.CommitVersion(ctx, CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: second.ID, ExpectedGeneration: 1, Actor: "usr_1", Model: store.ModelTree,
		Files: []contentpayload.File{{Path: "SKILL.md", Size: 5, Content: bytes.NewBufferString("hello")}},
	})
	require.NoError(t, err)
	authorized, err := NewAuthorizedService(core, ScopedAccessPolicy{})
	require.NoError(t, err)
	readContext := WithAccessPrincipal(ctx, AccessPrincipal{
		Subject: "usr_1", WorkspaceID: "wsp_1", Domain: "skill",
		BindingKey: "skill_1", ContentID: first.ID, Actions: []AccessAction{AccessReadPayload},
	})

	_, _, err = authorized.OpenFile(readContext, "wsp_1", first.ID, secondVersion.Version.ID, "SKILL.md", nil)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound)
	_, err = authorized.ListVersionPaths(readContext, "wsp_1", first.ID, secondVersion.Version.ID, store.MaxCompareEntries)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound)
}

func TestAuthorizedServiceListsOnlyExactImmutableVersionWithinBounds(t *testing.T) {
	core, _ := newServiceTest(t)
	ctx := context.Background()
	content, err := core.CreateContent(ctx, CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "knowledge_qa", BindingKey: "root_1",
		Model: store.ModelTree, Actor: "usr_1",
	})
	require.NoError(t, err)
	first, err := core.CommitVersion(ctx, CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: content.ID, ExpectedGeneration: 1,
		Actor: "usr_1", Model: store.ModelTree,
		Files: []contentpayload.File{{Path: "old.md", Size: 3, Content: bytes.NewBufferString("old")}},
	})
	require.NoError(t, err)
	_, err = core.CommitVersion(ctx, CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: content.ID,
		ExpectedHeadVersionID: first.Version.ID, ExpectedGeneration: 1,
		Actor: "usr_1", Model: store.ModelTree,
		Files: []contentpayload.File{{Path: "new.md", Size: 3, Content: bytes.NewBufferString("new")}},
	})
	require.NoError(t, err)
	authorized, err := NewAuthorizedService(core, ScopedAccessPolicy{})
	require.NoError(t, err)
	readContext := WithAccessPrincipal(ctx, AccessPrincipal{
		Subject: "usr_1", WorkspaceID: "wsp_1", Domain: "knowledge_qa",
		BindingKey: "root_1", ContentID: content.ID, Actions: []AccessAction{AccessReadPayload},
	})

	paths, err := authorized.ListVersionPaths(readContext, "wsp_1", content.ID, first.Version.ID, 1)
	require.NoError(t, err)
	require.Len(t, paths, 1)
	require.Equal(t, first.Version.ID, paths[0].VersionID)
	require.Equal(t, "old.md", paths[0].Path)
	_, err = authorized.ListVersionPaths(readContext, "wsp_1", content.ID, first.Version.ID, 0)
	require.ErrorContains(t, err, "path limit")
	_, err = authorized.ListVersionPaths(readContext, "wsp_1", content.ID, first.Version.ID, store.MaxCompareEntries+1)
	require.ErrorContains(t, err, "path limit")
	_, err = authorized.ListVersionPaths(readContext, "wsp_1", content.ID, "ctv_forged", 1)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound)
}

type unavailableAccessPolicy struct{ err error }

func (policy unavailableAccessPolicy) AuthorizeContentAccess(context.Context, AccessRequest) error {
	return policy.err
}

func TestAuthorizedServiceFailsClosedWhenPolicyIsUnavailable(t *testing.T) {
	core, _ := newServiceTest(t)
	content, err := core.CreateContent(context.Background(), CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_1", Model: store.ModelTree, Actor: "usr_1",
	})
	require.NoError(t, err)
	authorized, err := NewAuthorizedService(core, unavailableAccessPolicy{err: errors.New("grant store unavailable")})
	require.NoError(t, err)
	ctx := WithAccessPrincipal(context.Background(), AccessPrincipal{
		Subject: "usr_1", WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_1",
		Actions: []AccessAction{AccessReadMetadata},
	})

	_, err = authorized.GetContent(ctx, "wsp_1", content.ID)
	require.ErrorIs(t, err, ErrPolicyUnavailable)
}

func TestAuthorizedServiceSeparatesDetachedMigrationFromOrdinaryPublish(t *testing.T) {
	core, database := newServiceTest(t)
	ctx := context.Background()
	content, err := core.CreateContent(ctx, CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "memory", BindingKey: "mem_1", Model: store.ModelTree, Actor: "worker_1",
	})
	require.NoError(t, err)
	authorized, err := NewAuthorizedService(core, ScopedAccessPolicy{})
	require.NoError(t, err)
	request := PublishDetachedMigrationVersionRequest{
		WorkspaceID: "wsp_1", ContentID: content.ID, ExpectedGeneration: 1,
		Actor: "worker_1", Model: store.ModelTree, Checkpoint: true,
		Files:        []contentpayload.File{{Path: "MEMORY.md", Size: 6, Content: bytes.NewBufferString("memory")}},
		OperationKey: "content.migration:memory-1",
	}
	ordinary := WithAccessPrincipal(ctx, AccessPrincipal{
		Subject: "worker_1", WorkspaceID: "wsp_1", Domain: "memory", BindingKey: "mem_1", ContentID: content.ID,
		Generation: 1, Actions: []AccessAction{AccessPublish},
	})
	_, err = authorized.PublishDetachedMigrationVersion(ordinary, request)
	require.ErrorIs(t, err, ErrAccessDenied)
	var gcCount int64
	require.NoError(t, database.Model(&store.StagedPayloadGC{}).Where("content_id = ?", content.ID).Count(&gcCount).Error)
	require.Zero(t, gcCount, "ordinary publish authorization must fail before staging")

	detached := WithAccessPrincipal(ctx, AccessPrincipal{
		Subject: "worker_1", WorkspaceID: "wsp_1", Domain: "memory", BindingKey: "mem_1", ContentID: content.ID,
		Generation: 1, Actions: []AccessAction{AccessPublishDetachedMigration},
	})
	published, err := authorized.PublishDetachedMigrationVersion(detached, request)
	require.NoError(t, err)
	require.Empty(t, published.Content.HeadVersionID)
	receipt, found, err := authorized.FindDetachedMigrationVersionOperation(detached, "wsp_1", content.ID, request.OperationKey)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, published.Version.ID, receipt.VersionID)
	_, _, err = authorized.FindVersionOperation(detached, "wsp_1", content.ID, request.OperationKey)
	require.ErrorIs(t, err, ErrAccessDenied, "detached authorization does not imply ordinary publish access")
}
