package s3api

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"sync"
	"testing"
	"time"

	"github.com/TecharoHQ/anubis/lib/store/storetest"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

// mockS3 is an in-memory mock of the methods we use.
type mockS3 struct {
	mu     sync.RWMutex
	bucket string
	data   map[string][]byte
	meta   map[string]map[string]string
}

func (m *mockS3) PutObject(ctx context.Context, in *s3.PutObjectInput, _ ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.data == nil {
		m.data = map[string][]byte{}
	}
	if m.meta == nil {
		m.meta = map[string]map[string]string{}
	}
	b, _ := io.ReadAll(in.Body)
	m.data[aws.ToString(in.Key)] = bytes.Clone(b)
	if in.Metadata != nil {
		m.meta[aws.ToString(in.Key)] = map[string]string{}
		for k, v := range in.Metadata {
			m.meta[aws.ToString(in.Key)][k] = v
		}
	}
	m.bucket = aws.ToString(in.Bucket)
	return &s3.PutObjectOutput{}, nil
}

func (m *mockS3) GetObject(ctx context.Context, in *s3.GetObjectInput, _ ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	b, ok := m.data[aws.ToString(in.Key)]
	if !ok {
		return nil, fmt.Errorf("not found")
	}
	out := &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(b))}
	if md, ok := m.meta[aws.ToString(in.Key)]; ok {
		out.Metadata = md
	}
	return out, nil
}

func (m *mockS3) DeleteObject(ctx context.Context, in *s3.DeleteObjectInput, _ ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.data, aws.ToString(in.Key))
	delete(m.meta, aws.ToString(in.Key))
	return &s3.DeleteObjectOutput{}, nil
}

func (m *mockS3) HeadObject(ctx context.Context, in *s3.HeadObjectInput, _ ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if _, ok := m.data[aws.ToString(in.Key)]; !ok {
		return nil, fmt.Errorf("not found")
	}
	return &s3.HeadObjectOutput{}, nil
}

func TestImpl(t *testing.T) {
	mock := &mockS3{}
	f := Factory{Client: mock}

	data, _ := json.Marshal(Config{
		BucketName: "bucket",
	})

	storetest.Common(t, f, json.RawMessage(data))
}

func TestKeyNormalization(t *testing.T) {
	mock := &mockS3{}
	f := Factory{Client: mock}

	data, _ := json.Marshal(Config{
		BucketName: "anubis",
	})

	s, err := f.Build(t.Context(), json.RawMessage(data))
	if err != nil {
		t.Fatal(err)
	}

	key := "a:b:c"
	val := []byte("value")
	if err := s.Set(t.Context(), key, val, 0); err != nil {
		t.Fatalf("Set failed: %v", err)
	}
	// Ensure mock saw normalized key
	mock.mu.RLock()
	_, hasRaw := mock.data["a:b:c"]
	got, hasNorm := mock.data["a/b/c"]
	mock.mu.RUnlock()
	if hasRaw {
		t.Fatalf("mock contains raw key with colon; normalization failed")
	}
	if !hasNorm || !bytes.Equal(got, val) {
		t.Fatalf("normalized key missing or wrong value: got=%q", string(got))
	}

	// Get using colon key should work
	out, err := s.Get(t.Context(), key)
	if err != nil {
		t.Fatalf("Get failed: %v", err)
	}
	if !bytes.Equal(out, val) {
		t.Fatalf("Get returned wrong value: got=%q", string(out))
	}

	// Delete using colon key should delete normalized object
	if err := s.Delete(t.Context(), key); err != nil {
		t.Fatalf("Delete failed: %v", err)
	}
	// Give any async cleanup in tests a tick (not needed for mock, but harmless)
	time.Sleep(1 * time.Millisecond)
	mock.mu.RLock()
	_, exists := mock.data["a/b/c"]
	mock.mu.RUnlock()
	if exists {
		t.Fatalf("normalized key still exists after Delete")
	}
}
