package unity_catalog

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go-v2/service/sts"

	"github.com/seaweedfs/seaweedfs/test/testutil"
)

// TestUnityCatalogMasterRoleIntegration covers the master-role configuration
// the upstream playground notes as not-yet-working
// (https://github.com/data-engineering-helpers/mds-in-a-box/blob/main/unitycatalog-playground/etc/conf/server.properties#L45).
//
// It verifies two things end-to-end:
//
//  1. SeaweedFS' STS endpoint accepts sts:AssumeRole for the
//     UnityCatalogVendedRole, returning real STS-vended credentials. This
//     mirrors what the lakekeeper / polaris tests already exercise via the Go
//     AWS SDK and is the SeaweedFS-side prerequisite for UC's master-role
//     flow.
//
//  2. Unity Catalog OSS starts and accepts catalog/schema/EXTERNAL Delta
//     table CRUD when configured with `aws.masterRoleArn` set to that role
//     and AWS_ENDPOINT_URL_STS pointed at SeaweedFS.
//
// What it does NOT verify is the third hop -- UC's Java StsClient
// reaching SeaweedFS' STS handler. UC's
// AwsCredentialGenerator.StsAwsCredentialGenerator builds the StsClient
// without an endpointOverride and doesn't propagate aws.endpoint to it, so
// the SDK's generic AWS_ENDPOINT_URL_STS resolution doesn't kick in and the
// request always targets real AWS, returning InvalidClientTokenId. Verified
// by pointing AWS_ENDPOINT_URL_STS at port 1: same 403 either way, and a
// sniffer in front of SeaweedFS records zero traffic. UC's own tests mock
// StsClient out (BaseCRUDTestWithMockCredentials' EchoAwsStsClient,
// Mockito.mockStatic in CloudCredentialVendorTest), so this never showed
// up upstream. Fix is in unitycatalog/unitycatalog#1532; that bit is
// logged via t.Logf and not asserted.
func TestUnityCatalogMasterRoleIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in -short mode")
	}
	if !testutil.HasDocker() {
		t.Skip("docker not available")
	}

	env := newTestEnv(t)
	defer env.cleanup(t)

	t.Log(">>> starting SeaweedFS with STS-enabled IAM config...")
	env.startSeaweedFS(t, stsEnabledIAMConfig(env.accessKey, env.secretKey))
	t.Log(">>> SeaweedFS ready")

	ctx, cancel := context.WithTimeout(context.Background(), 8*time.Minute)
	defer cancel()

	s3c := env.newHostS3Client(t, ctx)
	if _, err := s3c.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(ucWarehouse)}); err != nil {
		t.Fatalf("create warehouse bucket: %v", err)
	}

	// Slice 1: prove SeaweedFS STS works for the role UC would use.
	t.Run("SeaweedFsAssumesUnityCatalogVendedRole", func(t *testing.T) {
		stsEndpoint := fmt.Sprintf("http://127.0.0.1:%d", env.s3Port)
		creds, err := assumeRoleViaSeaweedFS(ctx, stsEndpoint, env.accessKey, env.secretKey, ucVendedRoleArn)
		if err != nil {
			t.Fatalf("AssumeRole on SeaweedFS STS: %v", err)
		}
		if creds.SessionToken == "" {
			t.Fatalf("expected non-empty session_token from STS, got %+v", creds)
		}
		if creds.AccessKeyID == env.accessKey {
			t.Fatalf("STS returned the static admin access key; AssumeRole did not vend a new one")
		}

		// Confirm the vended creds actually work for an S3 round-trip.
		s3v := env.newHostS3ClientWithCreds(t, ctx, creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken)
		probeKey := "ns-probe/master-role-probe.txt"
		if _, err := s3v.PutObject(ctx, &s3.PutObjectInput{
			Bucket: aws.String(ucWarehouse),
			Key:    aws.String(probeKey),
			Body:   nil,
		}); err != nil {
			t.Fatalf("PutObject with sts-vended creds: %v", err)
		}
		_, _ = s3v.DeleteObject(ctx, &s3.DeleteObjectInput{
			Bucket: aws.String(ucWarehouse),
			Key:    aws.String(probeKey),
		})
	})

	// Slice 2: UC starts with master-role config and CRUD works.
	stsEndpoint := fmt.Sprintf("http://host.docker.internal:%d", env.s3Port)
	t.Log(">>> starting Unity Catalog server (master role)...")
	env.startUnityCatalog(t, ctx, ucServerOpts{
		MasterRoleArn: ucVendedRoleArn,
		ExtraEnv: map[string]string{
			"AWS_ENDPOINT_URL":      stsEndpoint,
			"AWS_ENDPOINT_URL_STS":  stsEndpoint,
			"AWS_ACCESS_KEY_ID":     env.accessKey,
			"AWS_SECRET_ACCESS_KEY": env.secretKey,
			"AWS_REGION":            "us-east-1",
		},
	})
	t.Log(">>> Unity Catalog ready")

	uc := newUCClient(fmt.Sprintf("http://127.0.0.1:%d", env.ucHostPort))
	suffix := time.Now().UnixNano()
	catalogName := fmt.Sprintf("seaweed_uc_role_%d", suffix)
	schemaName := fmt.Sprintf("ns_%d", suffix)
	tableName := fmt.Sprintf("events_%d", suffix)
	tableLocation := fmt.Sprintf("s3://%s/%s/%s/%s", ucWarehouse, ucWarehouseKey, schemaName, tableName)

	defer func() {
		_ = uc.deleteTable(context.Background(), catalogName+"."+schemaName+"."+tableName)
		_ = uc.deleteSchema(context.Background(), catalogName+"."+schemaName)
		_ = uc.deleteCatalog(context.Background(), catalogName)
	}()

	t.Run("UnityCatalogAcceptsMasterRoleConfig", func(t *testing.T) {
		if _, err := uc.createCatalog(ctx, ucCreateCatalog{
			Name:        catalogName,
			StorageRoot: fmt.Sprintf("s3://%s/%s", ucWarehouse, ucWarehouseKey),
		}); err != nil {
			t.Fatalf("create catalog: %v", err)
		}
		if _, err := uc.createSchema(ctx, ucCreateSchema{Name: schemaName, CatalogName: catalogName}); err != nil {
			t.Fatalf("create schema: %v", err)
		}
		created, err := uc.createTable(ctx, ucCreateTable{
			Name:             tableName,
			CatalogName:      catalogName,
			SchemaName:       schemaName,
			TableType:        "EXTERNAL",
			DataSourceFormat: "DELTA",
			Columns: []ucColumn{
				{Name: "id", TypeText: "long", TypeName: "LONG", TypeJSON: `{"name":"id","type":"long","nullable":true,"metadata":{}}`, Position: 0, Nullable: true},
			},
			StorageLocation: tableLocation,
		})
		if err != nil {
			t.Fatalf("create external delta table: %v", err)
		}
		if created.TableID == "" {
			t.Fatalf("expected table_id in CreateTable response, got %+v", created)
		}

		// The actual UC -> SeaweedFS STS handoff is the still-unsolved slice.
		// Log what happens, but don't fail the test on it: SeaweedFS' STS is
		// known-good (slice 1 above), and UC's CRUD is known-good (this slice).
		_, ucCredErr := uc.generateTemporaryTableCredentials(ctx, created.TableID, "READ_WRITE")
		if ucCredErr != nil {
			t.Logf("UC StsClient still hits real AWS (UC bug, fix in unitycatalog/unitycatalog#1532): %v", ucCredErr)
		} else {
			t.Logf("UC StsClient honors endpoint override; unitycatalog/unitycatalog#1532 (or equivalent) is in this image.")
		}
	})
}

// assumeRoleViaSeaweedFS calls sts:AssumeRole against SeaweedFS' STS endpoint
// using the Go AWS SDK, mirroring how lakekeeper/polaris tests prove
// SeaweedFS' STS path works.
func assumeRoleViaSeaweedFS(ctx context.Context, endpoint, accessKey, secretKey, roleArn string) (aws.Credentials, error) {
	cfg, err := config.LoadDefaultConfig(ctx,
		config.WithRegion("us-east-1"),
		config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
	)
	if err != nil {
		return aws.Credentials{}, fmt.Errorf("load aws config: %w", err)
	}

	// Override the STS endpoint via service-specific BaseEndpoint instead of
	// the deprecated EndpointResolverWithOptions wired through the global
	// config. This is the supported path in aws-sdk-go-v2 since v1.21.
	client := sts.NewFromConfig(cfg, func(o *sts.Options) {
		o.BaseEndpoint = aws.String(endpoint)
	})
	resp, err := client.AssumeRole(ctx, &sts.AssumeRoleInput{
		RoleArn:         aws.String(roleArn),
		RoleSessionName: aws.String("uc-master-role-test"),
	})
	if err != nil {
		return aws.Credentials{}, err
	}
	if resp.Credentials == nil {
		return aws.Credentials{}, fmt.Errorf("AssumeRole returned no credentials")
	}
	return aws.Credentials{
		AccessKeyID:     aws.ToString(resp.Credentials.AccessKeyId),
		SecretAccessKey: aws.ToString(resp.Credentials.SecretAccessKey),
		SessionToken:    aws.ToString(resp.Credentials.SessionToken),
		Source:          "seaweedfs-sts",
	}, nil
}
