package workbench

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"io"
	"os"
	"testing"

	"aigateway.local/core/internal/platform/config"
	"aigateway.local/core/internal/platform/database"
	"aigateway.local/core/internal/platform/storage"
)

// TestFileObjectLifecycle runs against real PostgreSQL + MinIO. Set
// WORKBENCH_TEST_DATABASE_URL and WORKBENCH_TEST_S3_ENDPOINT to enable it; with
// the deploy compose up these are reachable from the build container via the
// deploy_default network.
func TestFileObjectLifecycle(t *testing.T) {
	databaseURL := os.Getenv("WORKBENCH_TEST_DATABASE_URL")
	s3Endpoint := os.Getenv("WORKBENCH_TEST_S3_ENDPOINT")
	if databaseURL == "" || s3Endpoint == "" {
		t.Skip("WORKBENCH_TEST_DATABASE_URL and WORKBENCH_TEST_S3_ENDPOINT are not set")
	}
	ctx := context.Background()
	pool, err := database.Open(ctx, config.Database{URL: databaseURL, MaxConns: 8, MinConns: 0})
	if err != nil {
		t.Fatal(err)
	}
	defer pool.Close()

	store, err := storage.NewClient(storage.Config{
		Endpoint:        s3Endpoint,
		AccessKeyID:     os.Getenv("WORKBENCH_TEST_S3_ACCESS_KEY"),
		SecretAccessKey: os.Getenv("WORKBENCH_TEST_S3_SECRET_KEY"),
		Bucket:          os.Getenv("WORKBENCH_TEST_S3_BUCKET"),
		Region:          "us-east-1",
		MaxFileBytes:    8 << 20,
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := store.EnsureBucket(ctx); err != nil {
		t.Fatal(err)
	}
	adminID := "22222222-2222-4222-8222-222222222222"
	portalID := "33333333-3333-4333-8333-333333333333"
	if _, err := pool.Exec(ctx, `INSERT INTO gateway.admin_accounts(id,username,password_hash,role) VALUES($1,'m8-files-admin','test','superadmin') ON CONFLICT(id) DO NOTHING`, adminID); err != nil {
		t.Fatal(err)
	}
	if _, err := pool.Exec(ctx, `INSERT INTO gateway.portal_users(id,account,password_hash,name,active) VALUES($1,'m8-files-user','test','m8',true) ON CONFLICT(id) DO NOTHING`, portalID); err != nil {
		t.Fatal(err)
	}
	cleanup := func() {
		_, _ = pool.Exec(ctx, `DELETE FROM gateway.file_objects`)
	}
	cleanup()
	defer cleanup()

	files := NewFileService(NewService(pool), store)

	content := []byte("M8 对象存储文件管理集成测试 payload\nline two\n")
	owner := portalID
	sysObj, err := files.Upload(ctx, "system", nil, adminID, "报告.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", bytes.NewReader(content))
	if err != nil {
		t.Fatal(err)
	}
	personalObj, err := files.Upload(ctx, "personal", &owner, portalID, "notes.txt", "text/plain", bytes.NewReader(content))
	if err != nil {
		t.Fatal(err)
	}

	// sha256 往返一致
	hasher := sha256.Sum256(content)
	if personalObj.ContentSHA256 != hex.EncodeToString(hasher[:]) || personalObj.SizeBytes != int64(len(content)) {
		t.Fatalf("integrity mismatch: sha=%s size=%d", personalObj.ContentSHA256, personalObj.SizeBytes)
	}

	// 流式读回
	reader, size, err := files.Open(ctx, personalObj)
	if err != nil {
		t.Fatal(err)
	}
	got, err := io.ReadAll(reader)
	_ = reader.Close()
	if err != nil || int64(len(got)) != size || !bytes.Equal(got, content) {
		t.Fatalf("roundtrip mismatch: len=%d size=%d err=%v", len(got), size, err)
	}

	// 列表按范围隔离
	sysList, err := files.ListSystem(ctx)
	if err != nil || len(sysList) != 1 || sysList[0].ID != sysObj.ID {
		t.Fatalf("system list mismatch: %#v err=%v", sysList, err)
	}
	myList, err := files.ListPersonal(ctx, portalID)
	if err != nil || len(myList) != 1 || myList[0].ID != personalObj.ID {
		t.Fatalf("personal list mismatch: %#v err=%v", myList, err)
	}
	otherList, err := files.ListPersonal(ctx, "44444444-4444-4444-8444-444444444444")
	if err != nil || len(otherList) != 0 {
		t.Fatalf("other personal list should be empty: %#v err=%v", otherList, err)
	}

	// 归属隔离:A 不能读 B 的文件
	if _, err := files.GetPersonal(ctx, adminID, personalObj.ID); !errors.Is(err, ErrNotFound) {
		t.Fatalf("expected ErrNotFound for cross-owner read, got %v", err)
	}
	if _, err := files.GetSystem(ctx, personalObj.ID); !errors.Is(err, ErrNotFound) {
		t.Fatalf("expected ErrNotFound for system read of personal file, got %v", err)
	}

	// 超限上传被拒(8MiB 上限,用无 EOF 占位 reader 凑 9MiB)
	if _, err := files.Upload(ctx, "system", nil, adminID, "big.bin", "application/octet-stream", io.LimitReader(zeroReader{}, 9<<20)); err == nil {
		t.Fatal("expected oversized upload to fail")
	}

	// 删除后元数据与对象都消失
	if err := files.DeleteSystem(ctx, sysObj.ID); err != nil {
		t.Fatal(err)
	}
	if _, err := files.GetSystem(ctx, sysObj.ID); !errors.Is(err, ErrNotFound) {
		t.Fatalf("expected ErrNotFound after delete, got %v", err)
	}
	if err := files.DeletePersonal(ctx, portalID, personalObj.ID); err != nil {
		t.Fatal(err)
	}
	if _, err := files.GetPersonal(ctx, portalID, personalObj.ID); !errors.Is(err, ErrNotFound) {
		t.Fatalf("expected ErrNotFound after personal delete, got %v", err)
	}
}

type zeroReader struct{}

func (zeroReader) Read(p []byte) (int, error) {
	for i := range p {
		p[i] = 0
	}
	return len(p), nil
}
