package storage

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"errors"
	"io"
	"strings"
	"testing"
	"time"
)

func TestComposeStreams(t *testing.T) {
	objects := map[string]string{"a": "hello ", "b": "world"}
	var stored string
	hash, err := composeStreams(context.Background(), []string{"a", "b"}, func(_ context.Context, name string) (io.ReadCloser, error) {
		value, ok := objects[name]
		if !ok {
			return nil, errors.New("missing object")
		}
		return io.NopCloser(strings.NewReader(value)), nil
	}, func(_ context.Context, _ string, reader io.Reader) error {
		value, err := io.ReadAll(reader)
		stored = string(value)
		return err
	}, func(context.Context, string) error { return nil }, "out")
	if err != nil || stored != "hello world" {
		t.Fatalf("compose = %q, %v", stored, err)
	}
	want := md5.Sum([]byte(stored))
	if hash != hex.EncodeToString(want[:]) {
		t.Fatalf("hash = %q", hash)
	}
}

func TestComposeStreamsRemovesPartialDestination(t *testing.T) {
	removed := false
	_, err := composeStreams(context.Background(), []string{"missing"}, func(context.Context, string) (io.ReadCloser, error) {
		return nil, errors.New("open failed")
	}, func(_ context.Context, _ string, reader io.Reader) error {
		_, _ = io.ReadAll(reader)
		return nil
	}, func(context.Context, string) error {
		removed = true
		return nil
	}, "out")
	if err == nil || !removed {
		t.Fatalf("compose error = %v, removed = %v", err, removed)
	}
}

func TestComposeStreamsUnblocksProducerWhenDestinationFails(t *testing.T) {
	putErr := errors.New("destination failed")
	done := make(chan error, 1)
	go func() {
		_, err := composeStreams(context.Background(), []string{"large"}, func(context.Context, string) (io.ReadCloser, error) {
			return io.NopCloser(strings.NewReader(strings.Repeat("x", 1<<20))), nil
		}, func(context.Context, string, io.Reader) error {
			return putErr
		}, func(context.Context, string) error { return nil }, "out")
		done <- err
	}()

	select {
	case err := <-done:
		if !errors.Is(err, putErr) {
			t.Fatalf("compose error = %v, want %v", err, putErr)
		}
	case <-time.After(time.Second):
		t.Fatal("compose remained blocked after destination failure")
	}
}

func TestNormalizeDeletePrefix(t *testing.T) {
	tests := []struct {
		name    string
		value   string
		valid   string
		wantErr bool
	}{
		{name: "canonicalizes", value: "/uploads/chunks/", valid: "uploads/chunks"},
		{name: "cleans duplicate separators", value: "uploads//chunks", valid: "uploads/chunks"},
		{name: "rejects empty", value: " / ", wantErr: true},
		{name: "rejects traversal", value: "uploads/../", wantErr: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := normalizeDeletePrefix(tt.value)
			if tt.wantErr {
				if err == nil {
					t.Fatalf("normalizeDeletePrefix(%q) succeeded with %q", tt.value, got)
				}
				return
			}
			if err != nil || got != tt.valid {
				t.Fatalf("normalizeDeletePrefix(%q) = %q, %v; want %q", tt.value, got, err, tt.valid)
			}
		})
	}
}

func TestBoundedPrefixKeepsDirectoryBoundary(t *testing.T) {
	if got := boundedPrefix("uploads/chunks/1", "uploads/chunks/1/"); got != "uploads/chunks/1/" {
		t.Fatalf("boundedPrefix() = %q", got)
	}
	if got := boundedPrefix("uploads/chunks/1", "uploads/chunks/1"); got != "uploads/chunks/1" {
		t.Fatalf("boundedPrefix() changed ordinary prefix to %q", got)
	}
}

func TestAdvanceDeletePrefixCursor(t *testing.T) {
	if _, err := advanceDeletePrefixCursor("cursor", "cursor", true); err == nil {
		t.Fatal("same cursor should fail")
	}
	if next, err := advanceDeletePrefixCursor("", "next", true); err != nil || next != "next" {
		t.Fatalf("advance cursor = %q, %v", next, err)
	}
}
