package runtimeapi

import (
	"context"
	"io"
	"testing"
	"time"

	"github.com/nexuscrm/shared/pkg/contract"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/delegationv1"
	filessemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/filesv1"
	recordsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordsv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

type authorizedFilesCreateTestEngine struct {
	t         *testing.T
	body      []byte
	err       error
	download  *contract.FileDownload
	openErr   error
	deleteErr error
}

func (e *authorizedFilesCreateTestEngine) StoreRecordAttachment(
	ctx context.Context,
	request contract.FileUploadRequest,
) (*contract.FileUploadResult, error) {
	e.t.Helper()
	if _, ok := ctx.Deadline(); !ok {
		e.t.Fatal("file engine lost the RPC deadline")
	}
	if e.err != nil {
		return nil, e.err
	}
	body, err := io.ReadAll(request.Body)
	if err != nil {
		return nil, err
	}
	e.body = body
	return &contract.FileUploadResult{
		ID: request.ID, Name: request.FileName, Size: int64(len(body)), Mime: request.ContentType,
	}, nil
}

type retriableFileUploadTestError struct{}

func (*retriableFileUploadTestError) Error() string            { return "upload in progress" }
func (*retriableFileUploadTestError) RetriableCondition() bool { return true }

func (e *authorizedFilesCreateTestEngine) OpenRecordAttachment(
	context.Context,
	string,
	*models.UserSession,
) (*contract.FileDownload, bool, error) {
	return e.download, e.download != nil, e.openErr
}

func (e *authorizedFilesCreateTestEngine) DeleteRecordAttachment(
	context.Context,
	string,
	*models.UserSession,
) error {
	return e.deleteErr
}

func TestAuthorizedFilesCreateUsesContextBoundStreamState(t *testing.T) {
	engine := &authorizedFilesCreateTestEngine{t: t}
	server, err := NewAuthorizedFilesServer(engine)
	if err != nil {
		t.Fatal(err)
	}
	baseCtx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	ctx := context.WithValue(baseCtx, runtimeExecutionScopeContextKey{}, RuntimeExecutionScope{
		view:              queryResponseTestView(t),
		orgID:             testOrgID,
		appInstallationID: testInstallationID,
		workloadID:        testWorkloadID,
		capability:        filessemantic.CapabilityName,
		receipts: recordsemantic.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: filessemantic.CapabilityName,
			CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
		},
	})
	ctx = context.WithValue(ctx, trustedCallContextKey{}, newTrustedCall(delegationv1.Claims{
		DelegationID: "delegation-1", OrgID: testOrgID, PrincipalID: "user-1", SessionID: "session-1",
		CallerWorkloadID: testWorkloadID, AppInstallationID: testInstallationID,
		RuntimeAudience: "runtime-test", Capabilities: []string{filessemantic.CapabilityName},
		AuthEpoch: 2, SecurityEpoch: 3,
	}, models.UserSession{ID: "user-1"}))
	header, err := mapping.ToWireCreateFileChunk(filessemantic.CreateChunk{Header: &filessemantic.CreateHeader{
		IdempotencyKey: "upload-1", ParentObjectID: "WorkItem", ParentRecordID: "record-1",
		FileName: "proof.txt", ContentType: "text/plain", ContentLength: 5,
	}})
	if err != nil {
		t.Fatal(err)
	}
	data, err := mapping.ToWireCreateFileChunk(filessemantic.CreateChunk{Data: []byte("proof")})
	if err != nil {
		t.Fatal(err)
	}
	stream := &authorizedFilesCreateTestStream{
		ctx: ctx, requests: []*runtimev1.CreateFileRequest{header, data},
	}
	if err := server.CreateFile(stream); err != nil {
		t.Fatalf("CreateFile() error = %v", err)
	}
	if string(engine.body) != "proof" {
		t.Fatalf("stored body = %q, want proof", engine.body)
	}
	response, err := mapping.FromWireCreateFileResponse(stream.response)
	if err != nil {
		t.Fatal(err)
	}
	if response.FileName != "proof.txt" || response.Size != 5 {
		t.Fatalf("CreateFile() response = %#v", response)
	}
}

func TestDeterministicFileIDIsInstallationScoped(t *testing.T) {
	first := deterministicFileID("org-1", "installation-1", "upload-1")
	if repeated := deterministicFileID("org-1", "installation-1", "upload-1"); repeated != first {
		t.Fatalf("repeated file ID = %q, want %q", repeated, first)
	}
	for name, changed := range map[string]string{
		"Org":             deterministicFileID("org-2", "installation-1", "upload-1"),
		"AppInstallation": deterministicFileID("org-1", "installation-2", "upload-1"),
		"Idempotency key": deterministicFileID("org-1", "installation-1", "upload-2"),
	} {
		if changed == first {
			t.Fatalf("%s change reused file ID %q", name, first)
		}
	}
}

func TestAuthorizedFilesCreatePreservesEarlyRetriableEngineFailure(t *testing.T) {
	engine := &authorizedFilesCreateTestEngine{t: t, err: &retriableFileUploadTestError{}}
	server, err := NewAuthorizedFilesServer(engine)
	if err != nil {
		t.Fatal(err)
	}
	stream := authorizedFilesCreateTestStreamFor(t, []byte("proof"))
	if err := server.CreateFile(stream); status.Code(err) != codes.Unavailable {
		t.Fatalf("CreateFile() status = %v, want Unavailable", err)
	}
	if stream.ctx.Err() != nil {
		t.Fatalf("request context changed unexpectedly: %v", stream.ctx.Err())
	}
}

func TestAuthorizedFilesCreateReturnsEarlyEngineFailureWithoutWaitingForMoreClientFrames(t *testing.T) {
	engine := &authorizedFilesCreateTestEngine{t: t, err: &retriableFileUploadTestError{}}
	server, err := NewAuthorizedFilesServer(engine)
	if err != nil {
		t.Fatal(err)
	}
	stream := authorizedFilesCreateTestStreamFor(t, []byte("proof"))
	stream.requests = stream.requests[:1]
	wait := make(chan struct{})
	stream.waitAfterRequests = wait
	result := make(chan error, 1)
	go func() { result <- server.CreateFile(stream) }()

	select {
	case callErr := <-result:
		close(wait)
		if status.Code(callErr) != codes.Unavailable {
			t.Fatalf("CreateFile() status = %v, want Unavailable", callErr)
		}
	case <-time.After(100 * time.Millisecond):
		close(wait)
		t.Fatal("CreateFile waited for another client frame after the engine had failed")
	}
}

func TestAuthorizedFilesCreateMapsEngineIdempotencyMismatchToInvalidArgument(t *testing.T) {
	engine := &authorizedFilesCreateTestEngine{
		t:   t,
		err: sharedErrors.NewMalformedRequestError("file idempotency identity is bound to different content"),
	}
	server, err := NewAuthorizedFilesServer(engine)
	if err != nil {
		t.Fatal(err)
	}
	stream := authorizedFilesCreateTestStreamFor(t, []byte("proof"))
	if err := server.CreateFile(stream); status.Code(err) != codes.InvalidArgument {
		t.Fatalf("CreateFile() status = %v, want InvalidArgument", err)
	}
}

func TestAuthorizedFilesOpenMapsEngineMissingFileToNotFound(t *testing.T) {
	engine := &authorizedFilesCreateTestEngine{
		t:       t,
		openErr: sharedErrors.NewNotFoundWireError("File not found"),
	}
	server, err := NewAuthorizedFilesServer(engine)
	if err != nil {
		t.Fatal(err)
	}
	createStream := authorizedFilesCreateTestStreamFor(t, []byte("proof"))
	request, err := mapping.ToWireOpenFileRequest(
		createStream.ctx,
		filessemantic.OpenRequest{FileID: "file-1"},
	)
	if err != nil {
		t.Fatal(err)
	}
	stream := &authorizedFilesOpenTestStream{ctx: createStream.ctx}
	if err := server.OpenFile(request, stream); status.Code(err) != codes.NotFound {
		t.Fatalf("OpenFile() status = %v, want NotFound", err)
	}
}

func TestAuthorizedFilesDeleteMapsEngineMissingFileToNotFound(t *testing.T) {
	engine := &authorizedFilesCreateTestEngine{
		t:         t,
		deleteErr: sharedErrors.NewNotFoundWireError("File not found"),
	}
	server, err := NewAuthorizedFilesServer(engine)
	if err != nil {
		t.Fatal(err)
	}
	ctx := authorizedFilesCreateTestStreamFor(t, []byte("proof")).ctx
	request, err := mapping.ToWireDeleteFileRequest(
		ctx,
		filessemantic.DeleteRequest{FileID: "file-1"},
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := server.DeleteFile(ctx, request); status.Code(err) != codes.NotFound {
		t.Fatalf("DeleteFile() status = %v, want NotFound", err)
	}
}

type ownerCanceledFileReader struct{}

func (ownerCanceledFileReader) Read([]byte) (int, error) { return 0, context.Canceled }
func (ownerCanceledFileReader) Close() error             { return nil }

func TestAuthorizedFilesOpenDoesNotTreatReaderOwnerCancellationAsClientCancellation(t *testing.T) {
	engine := &authorizedFilesCreateTestEngine{
		t: t,
		download: &contract.FileDownload{
			FileName: "proof.txt", ContentType: "text/plain", Size: 5,
			Reader: ownerCanceledFileReader{},
		},
	}
	server, err := NewAuthorizedFilesServer(engine)
	if err != nil {
		t.Fatal(err)
	}
	createStream := authorizedFilesCreateTestStreamFor(t, []byte("proof"))
	request, err := mapping.ToWireOpenFileRequest(
		createStream.ctx,
		filessemantic.OpenRequest{FileID: "file-1"},
	)
	if err != nil {
		t.Fatal(err)
	}
	stream := &authorizedFilesOpenTestStream{ctx: createStream.ctx}
	if err := server.OpenFile(request, stream); status.Code(err) != codes.Unavailable {
		t.Fatalf("OpenFile() status = %v, want Unavailable", err)
	}
	if createStream.ctx.Err() != nil {
		t.Fatalf("request context changed unexpectedly: %v", createStream.ctx.Err())
	}
}

type authorizedFilesCreateTestStream struct {
	ctx               context.Context
	requests          []*runtimev1.CreateFileRequest
	waitAfterRequests <-chan struct{}
	response          *runtimev1.CreateFileResponse
}

func authorizedFilesCreateTestStreamFor(t *testing.T, body []byte) *authorizedFilesCreateTestStream {
	t.Helper()
	baseCtx, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	ctx := context.WithValue(baseCtx, runtimeExecutionScopeContextKey{}, RuntimeExecutionScope{
		view:              queryResponseTestView(t),
		orgID:             testOrgID,
		appInstallationID: testInstallationID,
		workloadID:        testWorkloadID,
		capability:        filessemantic.CapabilityName,
		receipts: recordsemantic.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: filessemantic.CapabilityName,
			CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
		},
	})
	ctx = context.WithValue(ctx, trustedCallContextKey{}, newTrustedCall(delegationv1.Claims{
		DelegationID: "delegation-1", OrgID: testOrgID, PrincipalID: "user-1", SessionID: "session-1",
		CallerWorkloadID: testWorkloadID, AppInstallationID: testInstallationID,
		RuntimeAudience: "runtime-test", Capabilities: []string{filessemantic.CapabilityName},
		AuthEpoch: 2, SecurityEpoch: 3,
	}, models.UserSession{ID: "user-1"}))
	header, err := mapping.ToWireCreateFileChunk(filessemantic.CreateChunk{Header: &filessemantic.CreateHeader{
		IdempotencyKey: "upload-1", ParentObjectID: "WorkItem", ParentRecordID: "record-1",
		FileName: "proof.txt", ContentType: "text/plain", ContentLength: uint64(len(body)),
	}})
	if err != nil {
		t.Fatal(err)
	}
	data, err := mapping.ToWireCreateFileChunk(filessemantic.CreateChunk{Data: body})
	if err != nil {
		t.Fatal(err)
	}
	return &authorizedFilesCreateTestStream{
		ctx: ctx, requests: []*runtimev1.CreateFileRequest{header, data},
	}
}

func (s *authorizedFilesCreateTestStream) Recv() (*runtimev1.CreateFileRequest, error) {
	if len(s.requests) == 0 {
		if s.waitAfterRequests != nil {
			select {
			case <-s.waitAfterRequests:
			case <-s.ctx.Done():
				return nil, s.ctx.Err()
			}
		}
		return nil, io.EOF
	}
	request := s.requests[0]
	s.requests = s.requests[1:]
	return request, nil
}

func (s *authorizedFilesCreateTestStream) SendAndClose(response *runtimev1.CreateFileResponse) error {
	s.response = response
	return nil
}

func (*authorizedFilesCreateTestStream) SetHeader(metadata.MD) error  { return nil }
func (*authorizedFilesCreateTestStream) SendHeader(metadata.MD) error { return nil }
func (*authorizedFilesCreateTestStream) SetTrailer(metadata.MD)       {}
func (s *authorizedFilesCreateTestStream) Context() context.Context   { return s.ctx }
func (*authorizedFilesCreateTestStream) SendMsg(any) error            { return nil }
func (*authorizedFilesCreateTestStream) RecvMsg(any) error            { return nil }

type authorizedFilesOpenTestStream struct {
	ctx context.Context
}

func (*authorizedFilesOpenTestStream) Send(*runtimev1.OpenFileResponse) error { return nil }
func (*authorizedFilesOpenTestStream) SetHeader(metadata.MD) error            { return nil }
func (*authorizedFilesOpenTestStream) SendHeader(metadata.MD) error           { return nil }
func (*authorizedFilesOpenTestStream) SetTrailer(metadata.MD)                 {}
func (s *authorizedFilesOpenTestStream) Context() context.Context             { return s.ctx }
func (*authorizedFilesOpenTestStream) SendMsg(any) error                      { return nil }
func (*authorizedFilesOpenTestStream) RecvMsg(any) error                      { return nil }
