package backend

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"testing"

	"github.com/mem9-ai/drive9/pkg/datastore"
)

type liveContextMetaQuotaStore struct {
	*fakeMetaQuotaStore
}

func (s *liveContextMetaQuotaStore) InsertMutationLog(ctx context.Context, entry *MutationLogView) (int64, error) {
	if err := ctx.Err(); err != nil {
		return 0, err
	}
	return s.fakeMetaQuotaStore.InsertMutationLog(ctx, entry)
}

func TestApplyCentralFileStateMovesBytesBetweenStorageTiers(t *testing.T) {
	store := newFakeMetaQuotaStore()
	db9 := StorageTypeDB9
	store.fileMeta[metaKey("tenant-a", "file-a")] = &FileMetaView{
		TenantID:    "tenant-a",
		FileID:      "file-a",
		SizeBytes:   80,
		StorageType: &db9,
	}

	deltas, err := applyCentralFileStateTx(store, nil, "tenant-a", "file-a", 120, false, StorageTypeS3)
	if err != nil {
		t.Fatalf("apply central file state: %v", err)
	}
	if deltas.storageBytes != 40 || deltas.dbStorageBytes != -80 || deltas.s3StorageBytes != 120 {
		t.Fatalf("deltas = %+v, want total=40 db9=-80 s3=120", deltas)
	}
	got, err := store.GetFileMeta(context.Background(), "tenant-a", "file-a")
	if err != nil {
		t.Fatalf("get file meta: %v", err)
	}
	if got.StorageType == nil || *got.StorageType != StorageTypeS3 {
		t.Fatalf("storage type = %v, want s3", got.StorageType)
	}
}

func TestHistoricalFileDeleteUsesShadowStateForTierDeltas(t *testing.T) {
	store := newFakeMetaQuotaStore()
	s3 := StorageTypeS3
	store.fileMeta[metaKey("tenant-a", "file-a")] = &FileMetaView{
		TenantID:    "tenant-a",
		FileID:      "file-a",
		SizeBytes:   41,
		IsMedia:     true,
		StorageType: &s3,
	}
	data, err := json.Marshal(fileDeleteMutationData{
		FileID:    "file-a",
		SizeBytes: 7,
		IsMedia:   false,
	})
	if err != nil {
		t.Fatal(err)
	}

	first, err := applyCentralQuotaMutationTx(context.Background(), store, nil, "tenant-a", "file_delete", data, 1)
	if err != nil {
		t.Fatalf("apply historical file delete: %v", err)
	}
	if first.storageBytes != -41 || first.dbStorageBytes != 0 || first.s3StorageBytes != -41 || first.fileCount != -1 || first.mediaFileCount != -1 {
		t.Fatalf("first deltas = %+v, want shadow-derived storage/s3=-41 and file/media=-1", first)
	}

	second, err := applyCentralQuotaMutationTx(context.Background(), store, nil, "tenant-a", "file_delete", data, 2)
	if err != nil {
		t.Fatalf("replay historical file delete: %v", err)
	}
	if second != (quotaCounterDeltas{}) {
		t.Fatalf("replayed deltas = %+v, want zero", second)
	}
}

func TestUploadCompleteUsesShadowStateForTotalAndTierDeltas(t *testing.T) {
	store := newFakeMetaQuotaStore()
	db9 := StorageTypeDB9
	store.fileMeta[metaKey("tenant-a", "file-a")] = &FileMetaView{
		TenantID:    "tenant-a",
		FileID:      "file-a",
		SizeBytes:   80,
		IsMedia:     true,
		StorageType: &db9,
	}

	deltas, err := applyUploadCompleteTx(context.Background(), store, nil, "tenant-a", uploadCompleteMutationData{
		UploadID:     "upload-a",
		FileID:       "file-a",
		OldSizeBytes: 5,
		OldIsMedia:   false,
		NewSizeBytes: 120,
		NewIsMedia:   false,
		StorageType:  StorageTypeS3,
	})
	if err != nil {
		t.Fatalf("apply upload complete: %v", err)
	}
	if deltas.storageBytes != 40 || deltas.dbStorageBytes != -80 || deltas.s3StorageBytes != 120 || deltas.fileCount != 0 || deltas.mediaFileCount != -1 {
		t.Fatalf("deltas = %+v, want shadow-derived total=40 db9=-80 s3=120 media=-1", deltas)
	}
}

func TestUploadCompleteReplacingLayerOwnedShadowKeepsLayerBytes(t *testing.T) {
	for _, tc := range []struct {
		name string
		old  int64
		new  int64
	}{
		{name: "same size", old: 100, new: 100},
		{name: "different size", old: 100, new: 60},
	} {
		t.Run(tc.name, func(t *testing.T) {
			store := newFakeMetaQuotaStore()
			s3 := StorageTypeS3
			store.fileMeta[metaKey("tenant-a", "layer-file")] = &FileMetaView{
				TenantID: "tenant-a", FileID: "layer-file", SizeBytes: tc.old,
				StorageType: &s3, LayerOwned: true,
			}

			deltas, err := applyUploadCompleteTx(context.Background(), store, nil, "tenant-a", uploadCompleteMutationData{
				UploadID: "upload-layer", FileID: "layer-file", NewSizeBytes: tc.new,
				StorageType: StorageTypeS3,
			})
			if err != nil {
				t.Fatalf("apply upload complete: %v", err)
			}
			if deltas.storageBytes != tc.new || deltas.dbStorageBytes != 0 || deltas.s3StorageBytes != tc.new {
				t.Fatalf("deltas = %+v, want new core bytes only: total=%d s3=%d", deltas, tc.new, tc.new)
			}
			got, err := store.GetFileMeta(context.Background(), "tenant-a", "layer-file")
			if err != nil {
				t.Fatalf("get file meta: %v", err)
			}
			if got.LayerOwned {
				t.Fatal("replacement shadow remains layer-owned")
			}
			if got.SizeBytes != tc.new || got.StorageType == nil || *got.StorageType != StorageTypeS3 {
				t.Fatalf("replacement shadow = %+v, want s3 size %d", got, tc.new)
			}
		})
	}
}

func TestRecordFSLayerStorageEmitsOnlyForFilePayloads(t *testing.T) {
	store := newFakeMetaQuotaStore()
	b := newBaseBackend(nil)
	b.metaStore = store
	b.tenantID = "tenant-a"

	if err := b.RecordFSLayerStorage(context.Background(), &datastore.FSLayerEntry{
		LayerID: "layer-a", PathHash: "hash-a", EntrySeq: 4, Op: datastore.FSLayerEntryOpUpsert,
		Kind: datastore.FSLayerEntryKindFile, ContentBlob: []byte("data"), SizeBytes: 4,
	}); err != nil {
		t.Fatal(err)
	}
	if err := b.RecordFSLayerStorage(context.Background(), &datastore.FSLayerEntry{
		LayerID: "layer-a", PathHash: "hash-a", EntrySeq: 5, Op: datastore.FSLayerEntryOpWhiteout,
		Kind: datastore.FSLayerEntryKindFile,
	}); err != nil {
		t.Fatal(err)
	}

	if len(store.mutations) != 1 {
		t.Fatalf("mutations = %+v, want one file payload mutation", store.mutations)
	}
	var got fsLayerStorageMutationData
	if err := json.Unmarshal(store.mutations[0].data, &got); err != nil {
		t.Fatal(err)
	}
	if got.LayerID != "layer-a" || got.PathHash != "hash-a" || got.EntrySeq != 4 || got.SizeBytes != 4 || got.StorageType != StorageTypeDB9 {
		t.Fatalf("payload = %+v, want inline db9 entry identity and size", got)
	}
}

func TestRecordFSLayerStorageDetachesFromCanceledRequest(t *testing.T) {
	store := &liveContextMetaQuotaStore{fakeMetaQuotaStore: newFakeMetaQuotaStore()}
	b := newBaseBackend(nil)
	b.metaStore = store
	b.tenantID = "tenant-a"
	requestCtx, cancelRequest := context.WithCancel(context.Background())
	cancelRequest()

	err := b.RecordFSLayerStorage(requestCtx, &datastore.FSLayerEntry{
		LayerID: "layer-a", PathHash: "hash-a", EntrySeq: 4, Op: datastore.FSLayerEntryOpUpsert,
		Kind: datastore.FSLayerEntryKindFile, ContentBlob: []byte("data"), SizeBytes: 4,
	})
	if err != nil {
		t.Fatalf("RecordFSLayerStorage after request cancellation: %v", err)
	}
	if len(store.mutations) != 1 {
		t.Fatalf("mutations=%d, want durable fs-layer storage handoff", len(store.mutations))
	}
}

func TestPutFSLayerObjectRejectsDeclaredSizeMismatch(t *testing.T) {
	for _, tc := range []struct {
		name     string
		payload  []byte
		declared int64
	}{
		{name: "short body", payload: []byte("short"), declared: 10},
		{name: "long body", payload: []byte("too-long"), declared: 3},
	} {
		t.Run(tc.name, func(t *testing.T) {
			b := newTestBackend(t)
			if _, err := b.PutFSLayerObject(context.Background(), "layer-a", "/data.bin", bytes.NewReader(tc.payload), tc.declared); err == nil {
				t.Fatal("PutFSLayerObject succeeded with mismatched declared size")
			}
		})
	}
}

func TestFSLayerStorageMutationCountsPayloadOnceWithoutCoreFileCounters(t *testing.T) {
	store := newFakeMetaQuotaStore()
	data, err := json.Marshal(fsLayerStorageMutationData{
		LayerID:     "layer-a",
		PathHash:    "hash-a",
		EntrySeq:    3,
		SizeBytes:   17,
		StorageType: StorageTypeS3,
	})
	if err != nil {
		t.Fatal(err)
	}

	first, err := applyCentralQuotaMutationTx(context.Background(), store, nil, "tenant-a", "fs_layer_store", data, 1)
	if err != nil {
		t.Fatal(err)
	}
	if first.storageBytes != 17 || first.dbStorageBytes != 0 || first.s3StorageBytes != 17 || first.fileCount != 0 || first.mediaFileCount != 0 {
		t.Fatalf("first deltas = %+v, want storage/s3=17 and no core file counters", first)
	}

	second, err := applyCentralQuotaMutationTx(context.Background(), store, nil, "tenant-a", "fs_layer_store", data, 2)
	if err != nil {
		t.Fatal(err)
	}
	if second != (quotaCounterDeltas{}) {
		t.Fatalf("duplicate deltas = %+v, want zero", second)
	}
}

func TestS3LayerCommitDoesNotCountAliasedCoreStorage(t *testing.T) {
	store := newFakeMetaQuotaStore()
	createData := []byte(`{"file_id":"core-file","size_bytes":17,"is_media":false,"storage_type":"s3","layer_owned":true}`)

	deltas, err := applyCentralQuotaMutationTx(context.Background(), store, nil, "tenant-a", "file_create", createData, 1)
	if err != nil {
		t.Fatalf("apply aliased core create: %v", err)
	}
	if deltas.storageBytes != 0 || deltas.s3StorageBytes != 0 || deltas.fileCount != 1 {
		t.Fatalf("aliased core deltas = %+v, want no storage bytes and one logical file", deltas)
	}

	overwriteData := []byte(`{"file_id":"core-file","old_size_bytes":17,"new_size_bytes":5,"old_is_media":false,"new_is_media":false,"storage_type":"db9"}`)
	deltas, err = applyCentralQuotaMutationTx(context.Background(), store, nil, "tenant-a", "file_overwrite", overwriteData, 2)
	if err != nil {
		t.Fatalf("apply physical core overwrite: %v", err)
	}
	if deltas.storageBytes != 5 || deltas.dbStorageBytes != 5 || deltas.s3StorageBytes != 0 {
		t.Fatalf("overwrite deltas = %+v, want only the new db9 copy counted", deltas)
	}

	deleteData := []byte(`{"file_id":"core-file","size_bytes":5,"is_media":false}`)
	deltas, err = applyCentralQuotaMutationTx(context.Background(), store, nil, "tenant-a", "file_delete", deleteData, 3)
	if err != nil {
		t.Fatalf("apply core delete: %v", err)
	}
	if deltas.storageBytes != -5 || deltas.dbStorageBytes != -5 || deltas.fileCount != -1 {
		t.Fatalf("delete deltas = %+v, want only the physical db9 bytes removed", deltas)
	}
}

func TestS3LayerCommitDoesNotReadmitAlreadyChargedPayloadBytes(t *testing.T) {
	b := newTestBackend(t)
	store := newFakeMetaQuotaStore()
	store.config["tenant-a"] = &QuotaConfigView{TenantID: "tenant-a", MaxStorageBytes: 17}
	store.usage["tenant-a"] = &QuotaUsageView{TenantID: "tenant-a", StorageBytes: 17}
	b.metaStore = store
	b.tenantID = "tenant-a"

	_, err := b.WriteStoredObjectCtxIfRevision(context.Background(), "/layer-object.bin", &datastore.FSLayerEntry{
		LayerID: "layer-a", Path: "/layer-object.bin", PathHash: "hash-a", EntrySeq: 1,
		Op: datastore.FSLayerEntryOpUpsert, Kind: datastore.FSLayerEntryKindFile,
		StorageType: string(datastore.StorageS3), StorageRef: "layers/layer-a/object", SizeBytes: 17,
	})
	if errors.Is(err, ErrStorageQuotaExceeded) {
		t.Fatalf("layer commit re-admitted payload bytes already charged by fs_layer_store: %v", err)
	}
	if err != nil {
		t.Fatal(err)
	}
	usage, err := store.GetQuotaUsage(context.Background(), "tenant-a")
	if err != nil {
		t.Fatal(err)
	}
	if usage.StorageBytes != 17 || usage.FileCount != 1 {
		t.Fatalf("usage after alias commit = %+v, want storage unchanged and one core file", usage)
	}
}

func TestApplyCentralFileStateDoesNotFabricateLegacyTier(t *testing.T) {
	store := newFakeMetaQuotaStore()
	store.fileMeta[metaKey("tenant-a", "legacy-file")] = &FileMetaView{
		TenantID:  "tenant-a",
		FileID:    "legacy-file",
		SizeBytes: 80,
	}

	deltas, err := applyCentralFileStateTx(store, nil, "tenant-a", "legacy-file", 120, false, StorageTypeS3)
	if err != nil {
		t.Fatalf("apply central file state: %v", err)
	}
	if deltas.storageBytes != 40 || deltas.dbStorageBytes != 0 || deltas.s3StorageBytes != 120 {
		t.Fatalf("deltas = %+v, want total=40 db9=0 s3=120", deltas)
	}
}

func TestInvalidStorageTypeRemainsUnclassified(t *testing.T) {
	if got := storageTypePtr(StorageType("future-tier")); got != nil {
		t.Fatalf("storageTypePtr(future-tier) = %v, want nil", got)
	}
	if got := storageTypePtr(StorageType("db9")); got == nil || *got != StorageTypeDB9 {
		t.Fatalf("storageTypePtr(db9) = %v, want db9", got)
	}
}

func TestUnknownFSLayerStorageMutationCountsTotalButRemainsUnclassified(t *testing.T) {
	store := newFakeMetaQuotaStore()
	data, err := json.Marshal(fsLayerStorageMutationData{
		LayerID: "layer-future", PathHash: "hash-future", EntrySeq: 8,
		SizeBytes: 19, StorageType: StorageType("future-tier"),
	})
	if err != nil {
		t.Fatal(err)
	}

	deltas, err := applyCentralQuotaMutationTx(context.Background(), store, nil, "tenant-a", "fs_layer_store", data, 1)
	if err != nil {
		t.Fatalf("apply unknown fs-layer storage mutation: %v", err)
	}
	if deltas.storageBytes != 19 || deltas.dbStorageBytes != 0 || deltas.s3StorageBytes != 0 {
		t.Fatalf("deltas = %+v, want total=19 and no classified tier bytes", deltas)
	}
	got := store.layerStorageMeta["tenant-a::layer-future::hash-future::8"]
	if got == nil || got.StorageType != "" {
		t.Fatalf("layer shadow = %+v, want unclassified storage type", got)
	}
}

func TestFSLayerCoreMutationStorageTypeUsesEntryTier(t *testing.T) {
	for _, tc := range []struct {
		name  string
		entry datastore.StorageType
		want  StorageType
	}{
		{name: "inline", entry: "", want: StorageTypeDB9},
		{name: "db9", entry: datastore.StorageDB9, want: StorageTypeDB9},
		{name: "s3", entry: datastore.StorageS3, want: StorageTypeS3},
		{name: "unknown", entry: datastore.StorageType("future-tier"), want: ""},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if got := fsLayerCoreStorageType(tc.entry); got != tc.want {
				t.Fatalf("fsLayerCoreStorageType(%q) = %q, want %q", tc.entry, got, tc.want)
			}
		})
	}
}
