//go:build integration

package files

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

	"bountyboard/internal/testutil"
)

func TestSaveOpenRoundTrip(t *testing.T) {
	db := testutil.MongoDB(t)
	s := NewStore(db, 25)
	ctx := context.Background()

	content := []byte("%PDF-1.4 fake pdf content for sniffing")
	meta, err := s.Save(ctx, ScopeTask, "01JOWNER", "spec.pdf", "", bytes.NewReader(content))
	if err != nil {
		t.Fatalf("Save: %v", err)
	}
	if meta.MimeType != "application/pdf" {
		t.Errorf("sniffed mime = %q, want application/pdf", meta.MimeType)
	}
	if meta.Size != int64(len(content)) {
		t.Errorf("size = %d, want %d", meta.Size, len(content))
	}
	wantSum := sha256.Sum256(content)
	if meta.SHA256 != hex.EncodeToString(wantSum[:]) {
		t.Errorf("sha256 mismatch")
	}

	rc, got, err := s.Open(ctx, meta.ID)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer rc.Close()
	data, err := io.ReadAll(rc)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(data, content) {
		t.Error("downloaded content differs")
	}
	if got.OwnerID != "01JOWNER" || got.Scope != ScopeTask || got.Name != "spec.pdf" {
		t.Errorf("metadata round trip: %+v", got)
	}
	if got.SHA256 != meta.SHA256 || got.MimeType != meta.MimeType {
		t.Errorf("stored metadata mismatch: %+v vs %+v", got, meta)
	}
}

func TestDeclaredMimeUsedWhenSniffInconclusive(t *testing.T) {
	db := testutil.MongoDB(t)
	s := NewStore(db, 25)

	meta, err := s.Save(context.Background(), ScopeChat, "01JOWNER", "data.bin",
		"application/x-custom", bytes.NewReader([]byte{0x01, 0x02, 0x03}))
	if err != nil {
		t.Fatal(err)
	}
	if meta.MimeType != "application/x-custom" {
		t.Errorf("mime = %q, want declared application/x-custom", meta.MimeType)
	}

	// But a confident sniff overrides a lying declaration.
	meta2, err := s.Save(context.Background(), ScopeChat, "01JOWNER", "fake.txt",
		"text/plain", strings.NewReader("\x89PNG\r\n\x1a\n123456789"))
	if err != nil {
		t.Fatal(err)
	}
	if meta2.MimeType != "image/png" {
		t.Errorf("mime = %q, want sniffed image/png", meta2.MimeType)
	}
}

func TestSizeLimitEnforced(t *testing.T) {
	db := testutil.MongoDB(t)
	s := NewStore(db, 1) // 1 MiB cap

	big := io.LimitReader(neverEnding('x'), 1<<20+1)
	_, err := s.Save(context.Background(), ScopeChat, "01JOWNER", "big.bin", "", big)
	if !errors.Is(err, ErrTooLarge) {
		t.Fatalf("want ErrTooLarge, got %v", err)
	}

	// No orphaned file documents may remain.
	n, err := s.bucket.GetFilesCollection().CountDocuments(context.Background(), struct{}{})
	if err != nil {
		t.Fatal(err)
	}
	if n != 0 {
		t.Errorf("oversize upload left %d file docs behind", n)
	}

	// Exactly at the limit is fine.
	ok := io.LimitReader(neverEnding('x'), 1<<20)
	if _, err := s.Save(context.Background(), ScopeChat, "01JOWNER", "exact.bin", "", ok); err != nil {
		t.Fatalf("upload at exactly the limit: %v", err)
	}
}

func TestOpenMissingFile(t *testing.T) {
	db := testutil.MongoDB(t)
	s := NewStore(db, 25)
	if _, _, err := s.Open(context.Background(), "01JNOPE"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("want ErrNotFound, got %v", err)
	}
}

func TestDelete(t *testing.T) {
	db := testutil.MongoDB(t)
	s := NewStore(db, 25)
	ctx := context.Background()

	meta, err := s.Save(ctx, ScopeAvatar, "01JOWNER", "a.txt", "", strings.NewReader("hello"))
	if err != nil {
		t.Fatal(err)
	}
	if err := s.Delete(ctx, meta.ID); err != nil {
		t.Fatal(err)
	}
	if _, _, err := s.Open(ctx, meta.ID); !errors.Is(err, ErrNotFound) {
		t.Fatalf("want ErrNotFound after delete, got %v", err)
	}
	if err := s.Delete(ctx, meta.ID); !errors.Is(err, ErrNotFound) {
		t.Fatalf("double delete: want ErrNotFound, got %v", err)
	}
}

// neverEnding is an infinite reader of one repeated byte.
type neverEnding byte

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