package blobstore_test

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"net"
	"testing"
	"time"

	"github.com/enmanuel/unibus/pkg/blobstore"
	"github.com/enmanuel/unibus/pkg/embeddednats"
	"github.com/nats-io/nats.go"
	"github.com/nats-io/nats.go/jetstream"
)

func objFreePort(t *testing.T) int {
	t.Helper()
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("free port: %v", err)
	}
	defer l.Close()
	return l.Addr().(*net.TCPAddr).Port
}

// newObjectStore boots a single-node embedded NATS with JetStream and returns a
// replicated (R1) Object Store backend over it.
func newObjectStore(t *testing.T) blobstore.Store {
	t.Helper()
	ns, err := embeddednats.StartServer(embeddednats.ServerConfig{
		StoreDir: t.TempDir(),
		Host:     "127.0.0.1",
		Port:     objFreePort(t),
	})
	if err != nil {
		t.Fatalf("embedded nats: %v", err)
	}
	nc, err := nats.Connect(ns.ClientURL())
	if err != nil {
		ns.Shutdown()
		t.Fatalf("nats connect: %v", err)
	}
	js, err := jetstream.New(nc)
	if err != nil {
		nc.Close()
		ns.Shutdown()
		t.Fatalf("jetstream: %v", err)
	}
	st, err := blobstore.NewObjectStore(js, blobstore.ObjectStoreConfig{Replicas: 1, OpTimeout: 5 * time.Second})
	if err != nil {
		nc.Close()
		ns.Shutdown()
		t.Fatalf("new object store: %v", err)
	}
	t.Cleanup(func() { nc.Close(); ns.Shutdown(); ns.WaitForShutdown() })
	return st
}

// TestObjectStoreRoundTrip is the golden path: put ciphertext, get it back by
// its hash, Has reports presence, and re-putting identical bytes returns the
// same address (content-addressed dedup).
func TestObjectStoreRoundTrip(t *testing.T) {
	s := newObjectStore(t)
	data := []byte("encrypted-media-ciphertext-payload")

	hash, err := s.Put(data)
	if err != nil {
		t.Fatalf("Put: %v", err)
	}
	want := hex.EncodeToString(sha256Sum(data))
	if hash != want {
		t.Fatalf("hash = %q, want sha256 hex %q", hash, want)
	}
	got, err := s.Get(hash)
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	if !bytes.Equal(got, data) {
		t.Fatalf("Get returned %q, want %q", got, data)
	}
	if !s.Has(hash) {
		t.Fatalf("Has should be true for a stored blob")
	}
	// Re-put identical bytes: same address, no error.
	hash2, err := s.Put(data)
	if err != nil || hash2 != hash {
		t.Fatalf("re-Put: hash2=%q err=%v (want %q)", hash2, err, hash)
	}
}

// TestObjectStoreMissing is the edge/error path: a hash that was never stored
// is absent and unreadable.
func TestObjectStoreMissing(t *testing.T) {
	s := newObjectStore(t)
	missing := hex.EncodeToString(sha256Sum([]byte("never stored")))
	if s.Has(missing) {
		t.Fatalf("Has should be false for an unknown hash")
	}
	if _, err := s.Get(missing); err == nil {
		t.Fatalf("Get of an unknown hash should error")
	}
}

// TestObjectStoreAddressMatchesDisk is the contract test: the Object Store and
// the disk backend address identical bytes to the IDENTICAL hash, so a client
// cannot tell which backend a node uses and a blob ref is portable across them.
func TestObjectStoreAddressMatchesDisk(t *testing.T) {
	obj := newObjectStore(t)
	disk, err := blobstore.New(t.TempDir())
	if err != nil {
		t.Fatalf("disk store: %v", err)
	}
	for _, payload := range [][]byte{[]byte("a"), []byte("longer ciphertext blob \x00\x01\x02"), {}} {
		oh, err := obj.Put(payload)
		if err != nil {
			t.Fatalf("object Put: %v", err)
		}
		dh, err := disk.Put(payload)
		if err != nil {
			t.Fatalf("disk Put: %v", err)
		}
		if oh != dh {
			t.Fatalf("address mismatch for %q: object=%q disk=%q", payload, oh, dh)
		}
	}
}

func sha256Sum(b []byte) []byte {
	sum := sha256.Sum256(b)
	return sum[:]
}
