package storage

import (
	"bytes"
	"context"
	"io"
	"os"
	"testing"
)

// ── Unit Tests (always run) ──────────────

func TestS3_NewS3_Validation(t *testing.T) {
	// Missing bucket
	_, err := NewS3(S3Config{
		AccessKey: "test",
		SecretKey: "test",
	})
	if err == nil {
		t.Error("expected error for missing bucket")
	}

	// Missing credentials
	_, err = NewS3(S3Config{
		Bucket: "test-bucket",
	})
	if err == nil {
		t.Error("expected error for missing credentials")
	}

	// Missing access key only
	_, err = NewS3(S3Config{
		Bucket:    "test-bucket",
		SecretKey: "test",
	})
	if err == nil {
		t.Error("expected error for missing access key")
	}
}

func TestS3_FullKey(t *testing.T) {
	tests := []struct {
		prefix string
		key    string
		want   string
	}{
		{"", "files/ch/f.txt", "files/ch/f.txt"},
		{"switchboard/", "files/ch/f.txt", "switchboard/files/ch/f.txt"},
		{"prefix", "files/ch/f.txt", "prefix/files/ch/f.txt"}, // trailing slash added by NewS3
	}

	for _, tt := range tests {
		s := &S3Store{prefix: tt.prefix}
		// Normalize prefix same as NewS3 does
		if s.prefix != "" && s.prefix[len(s.prefix)-1] != '/' {
			s.prefix += "/"
		}
		got := s.fullKey(tt.key)
		if got != tt.want {
			t.Errorf("fullKey(%q, %q) = %q, want %q", tt.prefix, tt.key, got, tt.want)
		}
	}
}

func TestS3_KeyValidation(t *testing.T) {
	// validateKey is shared between PVC and S3 — test with S3 context
	bad := []string{
		"",
		"../etc/passwd",
		"/absolute/path",
		"files/../../etc/shadow",
	}
	for _, key := range bad {
		if err := validateKey(key); err == nil {
			t.Errorf("validateKey(%q) should have failed", key)
		}
	}

	good := []string{
		"files/ch/f.txt",
		"files/test-1/att-abc_test.pdf",
		"processing/abc123/status.json",
	}
	for _, key := range good {
		if err := validateKey(key); err != nil {
			t.Errorf("validateKey(%q) failed: %v", key, err)
		}
	}
}

// ── Integration Tests (require live S3 endpoint) ──────────────
//
// Set these environment variables to run:
//
//	S3_TEST_ENDPOINT=localhost:9000
//	S3_TEST_BUCKET=switchboard-test
//	S3_TEST_ACCESS_KEY=minioadmin
//	S3_TEST_SECRET_KEY=minioadmin
//
// Example with MinIO:
//
//	docker run -d -p 9000:9000 -p 9001:9001 \
//	  -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
//	  minio/minio server /data --console-address :9001
//	mc alias set local http://localhost:9000 minioadmin minioadmin
//	mc mb local/switchboard-test

func testS3Store(t *testing.T) *S3Store {
	t.Helper()

	endpoint := os.Getenv("S3_TEST_ENDPOINT")
	bucket := os.Getenv("S3_TEST_BUCKET")
	accessKey := os.Getenv("S3_TEST_ACCESS_KEY")
	secretKey := os.Getenv("S3_TEST_SECRET_KEY")

	if endpoint == "" || bucket == "" {
		t.Skip("S3_TEST_ENDPOINT and S3_TEST_BUCKET not set — skipping S3 integration tests")
	}

	s, err := NewS3(S3Config{
		Endpoint:       endpoint,
		Bucket:         bucket,
		Region:         "us-east-1",
		AccessKey:      accessKey,
		SecretKey:      secretKey,
		Prefix:         "test-" + t.Name() + "/",
		ForcePathStyle: true,
	})
	if err != nil {
		t.Fatalf("NewS3: %v", err)
	}

	// Clean up prefix after test
	t.Cleanup(func() {
		ctx := context.Background()
		_ = s.DeletePrefix(ctx, "files")
		_ = s.DeletePrefix(ctx, "processing")
	})

	return s
}

func TestS3_Integration_PutGetRoundTrip(t *testing.T) {
	s := testS3Store(t)
	ctx := context.Background()

	data := []byte("hello, S3 storage world")
	key := "files/test-1/att-1_test.txt"

	err := s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "text/plain")
	if err != nil {
		t.Fatalf("Put: %v", err)
	}

	rc, size, ct, err := s.Get(ctx, key)
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	defer rc.Close()

	if size != int64(len(data)) {
		t.Errorf("size = %d, want %d", size, len(data))
	}
	if ct != "text/plain" {
		t.Errorf("content_type = %q, want %q", ct, "text/plain")
	}

	got, _ := io.ReadAll(rc)
	if !bytes.Equal(got, data) {
		t.Errorf("data = %q, want %q", got, data)
	}
}

func TestS3_Integration_GetNotFound(t *testing.T) {
	s := testS3Store(t)
	ctx := context.Background()

	_, _, _, err := s.Get(ctx, "files/nonexistent/file.txt")
	if err != ErrNotFound {
		t.Errorf("err = %v, want ErrNotFound", err)
	}
}

func TestS3_Integration_Delete(t *testing.T) {
	s := testS3Store(t)
	ctx := context.Background()

	key := "files/ch/delete-me.txt"
	_ = s.Put(ctx, key, bytes.NewReader([]byte("bye")), 3, "text/plain")

	err := s.Delete(ctx, key)
	if err != nil {
		t.Fatalf("Delete: %v", err)
	}

	exists, _ := s.Exists(ctx, key)
	if exists {
		t.Error("file still exists after Delete")
	}
}

func TestS3_Integration_DeleteIdempotent(t *testing.T) {
	s := testS3Store(t)
	ctx := context.Background()

	// S3 delete is inherently idempotent
	err := s.Delete(ctx, "files/no-such/file.txt")
	if err != nil {
		t.Errorf("Delete nonexistent: %v", err)
	}
}

func TestS3_Integration_DeletePrefix(t *testing.T) {
	s := testS3Store(t)
	ctx := context.Background()

	prefix := "files/test-abc/"
	for _, name := range []string{"a.txt", "b.png", "c.pdf"} {
		key := prefix + name
		_ = s.Put(ctx, key, bytes.NewReader([]byte("x")), 1, "text/plain")
	}

	other := "files/test-other/keep.txt"
	_ = s.Put(ctx, other, bytes.NewReader([]byte("y")), 1, "text/plain")

	err := s.DeletePrefix(ctx, "files/test-abc")
	if err != nil {
		t.Fatalf("DeletePrefix: %v", err)
	}

	for _, name := range []string{"a.txt", "b.png", "c.pdf"} {
		exists, _ := s.Exists(ctx, prefix+name)
		if exists {
			t.Errorf("%s still exists after DeletePrefix", name)
		}
	}

	exists, _ := s.Exists(ctx, other)
	if !exists {
		t.Error("other channel file was incorrectly deleted")
	}
}

func TestS3_Integration_Exists(t *testing.T) {
	s := testS3Store(t)
	ctx := context.Background()

	key := "files/ch/exists.txt"
	_ = s.Put(ctx, key, bytes.NewReader([]byte("hi")), 2, "text/plain")

	exists, err := s.Exists(ctx, key)
	if err != nil {
		t.Fatalf("Exists: %v", err)
	}
	if !exists {
		t.Error("Exists returned false for existing file")
	}

	exists, err = s.Exists(ctx, "files/ch/nope.txt")
	if err != nil {
		t.Fatalf("Exists (missing): %v", err)
	}
	if exists {
		t.Error("Exists returned true for missing file")
	}
}

func TestS3_Integration_Healthy(t *testing.T) {
	s := testS3Store(t)
	if err := s.Healthy(context.Background()); err != nil {
		t.Errorf("Healthy: %v", err)
	}
}

func TestS3_Integration_Stats(t *testing.T) {
	s := testS3Store(t)
	ctx := context.Background()

	// Add some files
	for i := 0; i < 3; i++ {
		key := "files/ch/" + string(rune('a'+i)) + ".txt"
		data := bytes.Repeat([]byte("x"), 100*(i+1))
		_ = s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "text/plain")
	}

	stats, err := s.Stats(ctx)
	if err != nil {
		t.Fatalf("Stats: %v", err)
	}
	if stats.TotalFiles != 3 {
		t.Errorf("files = %d, want 3", stats.TotalFiles)
	}
	if stats.TotalBytes != 600 {
		t.Errorf("bytes = %d, want 600", stats.TotalBytes)
	}
	if !stats.Healthy || !stats.Configured {
		t.Error("expected healthy + configured")
	}
	if stats.Backend != "s3" {
		t.Errorf("backend = %q, want s3", stats.Backend)
	}
}

func TestS3_Integration_PutUnknownSize(t *testing.T) {
	s := testS3Store(t)
	ctx := context.Background()

	data := []byte("unknown size upload")
	key := "files/ch/unknown-size.txt"

	// size=0 means unknown — S3 should handle via multipart
	err := s.Put(ctx, key, bytes.NewReader(data), 0, "text/plain")
	if err != nil {
		t.Fatalf("Put with size=0: %v", err)
	}

	rc, size, _, err := s.Get(ctx, key)
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	defer rc.Close()

	if size != int64(len(data)) {
		t.Errorf("size = %d, want %d", size, len(data))
	}
}
