package archive

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go-v2/service/s3"
	s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
	"github.com/aws/smithy-go"
	"github.com/stretchr/testify/require"
)

type stubS3Client struct {
	objects map[string][]byte
	puts    []*s3.PutObjectInput
	gets    []*s3.GetObjectInput
}

func (client *stubS3Client) GetObject(_ context.Context, params *s3.GetObjectInput, _ ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
	client.gets = append(client.gets, params)
	payload, ok := client.objects[deref(params.Key)]
	if !ok {
		return nil, &s3types.NoSuchKey{}
	}
	return &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(payload))}, nil
}

func (client *stubS3Client) PutObject(_ context.Context, params *s3.PutObjectInput, _ ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
	client.puts = append(client.puts, params)
	if params.IfNoneMatch != nil && deref(params.IfNoneMatch) == "*" {
		if _, ok := client.objects[deref(params.Key)]; ok {
			return nil, &smithy.GenericAPIError{Code: "PreconditionFailed", Message: "archive chunk already exists"}
		}
	}
	payload, err := io.ReadAll(params.Body)
	if err != nil {
		return nil, err
	}
	if client.objects == nil {
		client.objects = make(map[string][]byte)
	}
	client.objects[deref(params.Key)] = payload
	return &s3.PutObjectOutput{}, nil
}

func TestChunkBoundariesAndObjectKey(t *testing.T) {
	t.Parallel()

	chunkStartSeq, err := ChunkStartSeq(1456)
	require.NoError(t, err)
	require.EqualValues(t, 1001, chunkStartSeq)

	key, err := ObjectKey("agentteam/archive", "ws_123", "thr_456", chunkStartSeq)
	require.NoError(t, err)
	require.Equal(t, "agentteam/archive/workspaces/ws_123/threads/thr_456/chunks/1001.ndjson.zst", key)
}

func TestFilesystemRoundTrip(t *testing.T) {
	t.Parallel()

	storage, err := NewFilesystem(t.TempDir())
	require.NoError(t, err)

	messages := testChunkMessages(1001, "agent_alpha")
	err = storage.WriteChunk(context.Background(), "ws_archive", "thr_archive", 1001, messages)
	require.NoError(t, err)
	require.NoError(t, storage.WriteChunk(context.Background(), "ws_archive", "thr_archive", 1001, messages))

	overwritten := make([]Message, len(messages))
	copy(overwritten, messages)
	overwritten[0].Content = "changed content"
	require.ErrorIs(t, storage.WriteChunk(context.Background(), "ws_archive", "thr_archive", 1001, overwritten), ErrChunkAlreadyExists)

	loaded, err := storage.ReadChunk(context.Background(), "ws_archive", "thr_archive", 1001)
	require.NoError(t, err)
	require.Equal(t, messages, loaded)
}

func TestReadRangeAcrossChunks(t *testing.T) {
	t.Parallel()

	storage, err := NewFilesystem(t.TempDir())
	require.NoError(t, err)
	require.NoError(t, storage.WriteChunk(context.Background(), "ws_range", "thr_range", 1, testChunkMessages(1, "member_a")))
	require.NoError(t, storage.WriteChunk(context.Background(), "ws_range", "thr_range", 1001, testChunkMessages(1001, "member_b")))

	ascending, err := ReadRange(context.Background(), storage, "ws_range", "thr_range", 995, 1005, ReadAscending, 20)
	require.NoError(t, err)
	require.Len(t, ascending, 11)
	require.EqualValues(t, 995, ascending[0].Seq)
	require.EqualValues(t, 1005, ascending[len(ascending)-1].Seq)

	descending, err := ReadRange(context.Background(), storage, "ws_range", "thr_range", 995, 1005, ReadDescending, 4)
	require.NoError(t, err)
	require.Len(t, descending, 4)
	require.EqualValues(t, 1005, descending[0].Seq)
	require.EqualValues(t, 1002, descending[3].Seq)
}

func TestS3RoundTripUsesDeterministicKey(t *testing.T) {
	t.Parallel()

	client := &stubS3Client{objects: make(map[string][]byte)}
	storage := newS3Storage("archive-bucket", "agentteam/archive", client)
	messages := testChunkMessages(1, "member_s3")

	err := storage.WriteChunk(context.Background(), "ws_s3", "thr_s3", 1, messages)
	require.NoError(t, err)
	require.Len(t, client.puts, 1)
	require.Equal(t, "archive-bucket", deref(client.puts[0].Bucket))
	require.Equal(t, "agentteam/archive/workspaces/ws_s3/threads/thr_s3/chunks/1.ndjson.zst", deref(client.puts[0].Key))
	require.Equal(t, s3types.ChecksumAlgorithmSha256, client.puts[0].ChecksumAlgorithm)
	require.Equal(t, "*", deref(client.puts[0].IfNoneMatch))

	loaded, err := storage.ReadChunk(context.Background(), "ws_s3", "thr_s3", 1)
	require.NoError(t, err)
	require.Equal(t, messages, loaded)
}

func TestS3WriteChunkIsIdempotentAndImmutable(t *testing.T) {
	t.Parallel()

	client := &stubS3Client{objects: make(map[string][]byte)}
	storage := newS3Storage("archive-bucket", "", client)
	messages := testChunkMessages(1, "member_s3")

	require.NoError(t, storage.WriteChunk(context.Background(), "ws_s3", "thr_s3", 1, messages))
	require.NoError(t, storage.WriteChunk(context.Background(), "ws_s3", "thr_s3", 1, messages))

	overwritten := make([]Message, len(messages))
	copy(overwritten, messages)
	overwritten[0].Content = "changed content"
	require.ErrorIs(t, storage.WriteChunk(context.Background(), "ws_s3", "thr_s3", 1, overwritten), ErrChunkAlreadyExists)
}

func TestS3MapsMissingChunkToDomainError(t *testing.T) {
	t.Parallel()

	client := &stubS3Client{objects: make(map[string][]byte)}
	storage := newS3Storage("archive-bucket", "", client)

	_, err := storage.ReadChunk(context.Background(), "ws_s3", "thr_missing", 1)
	require.ErrorIs(t, err, ErrChunkNotFound)
}

func testChunkMessages(chunkStartSeq int64, senderName string) []Message {
	messages := make([]Message, 0, ChunkMessageCount)
	base := time.Date(2026, 3, 10, 12, 0, 0, 0, time.UTC)
	for index := int64(0); index < ChunkMessageCount; index++ {
		seq := chunkStartSeq + index
		messages = append(messages, Message{
			ID:         fmt.Sprintf("msg_%d", seq),
			Seq:        seq,
			SenderType: "human",
			SenderName: senderName,
			Content:    senderName + " message",
			Timestamp:  base.Add(time.Duration(index) * time.Second),
		})
	}
	return messages
}

func deref(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}
