package iceberg

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

	"github.com/google/uuid"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
)

func TestDecodeCommitTransactionRequest(t *testing.T) {
	body := `{"table-changes":[
		{"identifier":{"namespace":["ns"],"name":"t1"},"requirements":[{"type":"assert-table-uuid","uuid":"00000000-0000-0000-0000-000000000001"}],"updates":[{"action":"set-properties","updates":{"k":"v"}}]},
		{"identifier":{"namespace":["ns","sub"],"name":"t2"},"requirements":[],"updates":[]}
	]}`

	var req CommitTransactionRequest
	if err := json.Unmarshal([]byte(body), &req); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}
	if len(req.TableChanges) != 2 {
		t.Fatalf("table-changes = %d, want 2", len(req.TableChanges))
	}
	if req.TableChanges[0].Identifier.Name != "t1" {
		t.Fatalf("change[0] name = %q, want t1", req.TableChanges[0].Identifier.Name)
	}
	if got := []string(req.TableChanges[1].Identifier.Namespace); len(got) != 2 || got[1] != "sub" {
		t.Fatalf("change[1] namespace = %v, want [ns sub]", got)
	}
}

func TestParseTableChangeSeparatesStatistics(t *testing.T) {
	change := tableChangeRequest{
		Requirements: json.RawMessage(`[{"type":"assert-table-uuid","uuid":"00000000-0000-0000-0000-000000000001"}]`),
		Updates: []json.RawMessage{
			json.RawMessage(`{"action":"set-properties","updates":{"k":"v"}}`),
			json.RawMessage(`{"action":"set-statistics","snapshot-id":10,"statistics-path":"s3://bucket/table/metadata/stats.puffin","file-size-in-bytes":100,"file-footer-size-in-bytes":20,"blob-metadata":[]}`),
		},
	}

	requirements, updates, statistics, reqErr := parseTableChange(change)
	if reqErr != nil {
		t.Fatalf("parseTableChange() error = %+v", reqErr)
	}
	if len(requirements) != 1 {
		t.Fatalf("requirements = %d, want 1", len(requirements))
	}
	if len(updates) != 1 {
		t.Fatalf("updates = %d, want 1", len(updates))
	}
	if len(statistics) != 1 || statistics[0].set == nil || statistics[0].set.SnapshotID != 10 {
		t.Fatalf("unexpected statistics updates: %#v", statistics)
	}
}

func TestParseTableChangeRejectsInvalidRequirements(t *testing.T) {
	change := tableChangeRequest{Requirements: json.RawMessage(`{not json}`)}
	_, _, _, reqErr := parseTableChange(change)
	if reqErr == nil {
		t.Fatalf("parseTableChange() expected error for invalid requirements")
	}
	if reqErr.status != 400 {
		t.Fatalf("status = %d, want 400", reqErr.status)
	}
}

func TestCloneTableMetadataIsIndependent(t *testing.T) {
	orig := &s3tables.TableMetadata{
		Iceberg:      &s3tables.IcebergMetadata{TableUUID: "uuid-1"},
		FullMetadata: json.RawMessage(`{"v":1}`),
	}
	clone := cloneTableMetadata(orig)
	if clone == orig || clone.Iceberg == orig.Iceberg {
		t.Fatalf("cloneTableMetadata returned aliased pointers")
	}

	orig.Iceberg.TableUUID = "mutated"
	orig.FullMetadata[0] = 'X'
	if clone.Iceberg.TableUUID != "uuid-1" {
		t.Fatalf("clone Iceberg.TableUUID = %q, want uuid-1", clone.Iceberg.TableUUID)
	}
	if string(clone.FullMetadata) != `{"v":1}` {
		t.Fatalf("clone FullMetadata = %q, mutated with source", clone.FullMetadata)
	}

	if cloneTableMetadata(nil) != nil {
		t.Fatalf("cloneTableMetadata(nil) = non-nil")
	}
}

func TestBuildTableRestoreRequestCarriesPriorState(t *testing.T) {
	pc := &preparedTableCommit{
		namespace:           []string{"ns", "sub"},
		tableName:           "t1",
		prevMetadataLoc:     "s3://bucket/ns/t1/metadata/v3.metadata.json",
		prevMetadataVersion: 3,
		prevMetadata: &s3tables.TableMetadata{
			Iceberg:      &s3tables.IcebergMetadata{TableUUID: uuid.NewString()},
			FullMetadata: json.RawMessage(`{"format-version":2,"last-updated-ms":3}`),
		},
	}

	req := buildTableRestoreRequest("arn:bucket", pc)

	if req.TableBucketARN != "arn:bucket" || req.Name != "t1" {
		t.Fatalf("restore target = %s/%s, want arn:bucket/t1", req.TableBucketARN, req.Name)
	}
	if req.MetadataLocation != pc.prevMetadataLoc {
		t.Fatalf("restore MetadataLocation = %q, want %q", req.MetadataLocation, pc.prevMetadataLoc)
	}
	if req.MetadataVersion != pc.prevMetadataVersion {
		t.Fatalf("restore MetadataVersion = %d, want %d", req.MetadataVersion, pc.prevMetadataVersion)
	}
	if req.Metadata == nil || !bytes.Equal(req.Metadata.FullMetadata, pc.prevMetadata.FullMetadata) {
		t.Fatalf("restore FullMetadata = %q, want %q", req.Metadata.FullMetadata, pc.prevMetadata.FullMetadata)
	}
	if req.Metadata.Iceberg == nil || req.Metadata.Iceberg.TableUUID != pc.prevMetadata.Iceberg.TableUUID {
		t.Fatalf("restore TableUUID mismatch")
	}
}

// TestRollbackRevertsEveryFlippedField mirrors handleUpdateTable's partial-field
// update semantics to prove that after a flip + rollback the table is back to its
// pre-transaction state. With the old location-only rollback the table kept the
// new full metadata and version; the restore request must revert all of them.
func TestRollbackRevertsEveryFlippedField(t *testing.T) {
	// prior on-disk state of an already-committed table.
	prior := tableEntry{
		metadataLocation: "s3://bucket/ns/t1/metadata/v3.metadata.json",
		metadataVersion:  3,
		metadata: &s3tables.TableMetadata{
			Iceberg:      &s3tables.IcebergMetadata{TableUUID: "uuid-1"},
			FullMetadata: json.RawMessage(`{"format-version":2,"v":3}`),
		},
	}

	pc := &preparedTableCommit{
		namespace:           []string{"ns"},
		tableName:           "t1",
		tableUUID:           uuid.MustParse("00000000-0000-0000-0000-000000000001"),
		metadataBytes:       json.RawMessage(`{"format-version":2,"v":4}`),
		metadataVersion:     4,
		newMetadataLoc:      "s3://bucket/ns/t1/metadata/v4.metadata.json",
		prevMetadataLoc:     prior.metadataLocation,
		prevMetadataVersion: prior.metadataVersion,
		prevMetadata:        cloneTableMetadata(prior.metadata),
	}

	// flip: apply the new commit, then roll it back via the restore request.
	state := prior.clone()
	state.applyUpdate(&s3tables.UpdateTableRequest{
		Metadata:         &s3tables.TableMetadata{Iceberg: &s3tables.IcebergMetadata{TableUUID: pc.tableUUID.String()}, FullMetadata: pc.metadataBytes},
		MetadataVersion:  pc.metadataVersion,
		MetadataLocation: pc.newMetadataLoc,
	})
	state.applyUpdate(buildTableRestoreRequest("arn:bucket", pc))

	if state.metadataLocation != prior.metadataLocation {
		t.Fatalf("after rollback location = %q, want %q", state.metadataLocation, prior.metadataLocation)
	}
	if state.metadataVersion != prior.metadataVersion {
		t.Fatalf("after rollback version = %d, want %d", state.metadataVersion, prior.metadataVersion)
	}
	if !bytes.Equal(state.metadata.FullMetadata, prior.metadata.FullMetadata) {
		t.Fatalf("after rollback FullMetadata = %q, want %q", state.metadata.FullMetadata, prior.metadata.FullMetadata)
	}
	if state.metadata.Iceberg.TableUUID != prior.metadata.Iceberg.TableUUID {
		t.Fatalf("after rollback TableUUID = %q, want %q", state.metadata.Iceberg.TableUUID, prior.metadata.Iceberg.TableUUID)
	}
}

// TestCleanupSafeMetadataKeepsFailedRollback proves a flip failure at index 2
// where table 0's rollback also fails leaves table 0's new metadata in place
// (its pointer still references it), while the rolled-back table 1 and the
// never-flipped tables 2 and 3 are safe to delete.
func TestCleanupSafeMetadataKeepsFailedRollback(t *testing.T) {
	prepared := make([]preparedTableCommit, 4)
	rolledBack := []bool{false, true} // tables 0 and 1 were flipped; 0's rollback failed

	safe := cleanupSafeMetadata(prepared, 2, rolledBack)

	want := []bool{false, true, true, true}
	for i := range want {
		if safe[i] != want[i] {
			t.Fatalf("safe[%d] = %v, want %v (full %v)", i, safe[i], want[i], safe)
		}
	}
}

// TestCleanupSafeMetadataFirstFlipFails covers a flip failure at the first table:
// nothing was flipped, so every written file is safe to delete.
func TestCleanupSafeMetadataFirstFlipFails(t *testing.T) {
	prepared := make([]preparedTableCommit, 3)
	safe := cleanupSafeMetadata(prepared, 0, nil)
	for i, ok := range safe {
		if !ok {
			t.Fatalf("safe[%d] = false, want true (full %v)", i, safe)
		}
	}
}

// TestCleanupSafeMetadataAllRolledBack covers the common case where every prior
// flip rolls back cleanly, so all metadata files are safe to delete.
func TestCleanupSafeMetadataAllRolledBack(t *testing.T) {
	prepared := make([]preparedTableCommit, 3)
	safe := cleanupSafeMetadata(prepared, 2, []bool{true, true})
	for i, ok := range safe {
		if !ok {
			t.Fatalf("safe[%d] = false, want true (full %v)", i, safe)
		}
	}
}

// tableEntry models the persisted table fields and the partial-update rules
// handleUpdateTable applies, so rollback can be exercised without a live filer.
type tableEntry struct {
	metadataLocation string
	metadataVersion  int
	metadata         *s3tables.TableMetadata
}

func (e tableEntry) clone() *tableEntry {
	return &tableEntry{
		metadataLocation: e.metadataLocation,
		metadataVersion:  e.metadataVersion,
		metadata:         cloneTableMetadata(e.metadata),
	}
}

func (e *tableEntry) applyUpdate(req *s3tables.UpdateTableRequest) {
	if req.Metadata != nil {
		if e.metadata == nil {
			e.metadata = &s3tables.TableMetadata{}
		}
		if req.Metadata.Iceberg != nil {
			if e.metadata.Iceberg == nil {
				e.metadata.Iceberg = &s3tables.IcebergMetadata{}
			}
			if req.Metadata.Iceberg.TableUUID != "" {
				e.metadata.Iceberg.TableUUID = req.Metadata.Iceberg.TableUUID
			}
		}
		if len(req.Metadata.FullMetadata) > 0 {
			e.metadata.FullMetadata = req.Metadata.FullMetadata
		}
	}
	if req.MetadataLocation != "" {
		e.metadataLocation = req.MetadataLocation
	}
	if req.MetadataVersion > 0 {
		e.metadataVersion = req.MetadataVersion
	}
}
