package iceberg

import (
	"bytes"
	"context"
	"fmt"
	"path"
	"testing"
	"time"

	"github.com/apache/iceberg-go"
	"github.com/apache/iceberg-go/table"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest"
)

// Merging manifests a foreign producer wrote used to fail for good on a
// day-partitioned table, whichever way round the producer spelled the
// partition union: nothing between reading an entry and writing it looks at
// its partition, so the time.Time the Avro decoder produced is still there
// when the manifest writer, which has no date logical type for a day
// partition, tries to encode it.
func TestRewriteManifestsNormalizesForeignDayPartitions(t *testing.T) {
	for _, valueFirst := range []bool{true, false} {
		name := "null-first partition union"
		if valueFirst {
			name = "value-first partition union"
		}
		t.Run(name, func(t *testing.T) {
			rewriteForeignDayPartitions(t, valueFirst)
		})
	}
}

func rewriteForeignDayPartitions(t *testing.T, valueFirst bool) {
	t.Helper()
	fs, client := startFakeFiler(t)

	schema := iceberg.NewSchema(0,
		iceberg.NestedField{ID: 1, Name: "event_time", Type: iceberg.PrimitiveTypes.Timestamp, Required: true},
	)
	spec := iceberg.NewPartitionSpec(iceberg.PartitionField{
		SourceIDs: []int{1}, FieldID: 1000, Name: "event_time_day", Transform: iceberg.DayTransform{},
	})
	setup := tableSetup{
		BucketName: "test-bucket",
		Namespace:  "analytics",
		TableName:  "events",
		Schema:     schema,
		Spec:       &spec,
		Snapshots: []table.Snapshot{{
			SnapshotID:   1,
			TimestampMs:  time.Now().UnixMilli(),
			ManifestList: "metadata/snap-1.avro",
		}},
	}
	meta := populateTable(t, fs, setup)

	metaDir := path.Join(s3tables.TablesPath, setup.BucketName, setup.tablePath(), "metadata")
	wantDays := make(map[string]iceberg.Date)
	var manifests []iceberg.ManifestFile
	for i := 0; i < 3; i++ {
		day := iceberg.Date(20737 + i)
		filePath := setup.fileRef("data", fmt.Sprintf("foreign-%d.parquet", i))
		dfBuilder, err := iceberg.NewDataFileBuilder(spec, iceberg.EntryContentData, filePath, iceberg.ParquetFile,
			map[int]any{1000: day}, nil, nil, 1, 1)
		if err != nil {
			t.Fatalf("build data file %d: %v", i, err)
		}
		snapshotID := int64(1)
		entry := iceberg.NewManifestEntry(iceberg.EntryStatusADDED, &snapshotID, nil, nil, dfBuilder.Build())

		manifestName := fmt.Sprintf("foreign-manifest-%d.avro", i)
		foreignBytes, manifest := s3tablestest.ForeignPartitionManifest(t, schema, spec, entry,
			setup.fileRef("metadata", manifestName), "date", time.Unix(int64(day)*24*60*60, 0).UTC(), valueFirst)
		fs.putEntry(metaDir, manifestName, &filer_pb.Entry{
			Name: manifestName, Attributes: &filer_pb.FuseAttributes{Mtime: time.Now().Unix()}, Content: foreignBytes,
		})
		manifests = append(manifests, manifest)
		wantDays[filePath] = day
	}

	var manifestList bytes.Buffer
	seqNum := int64(1)
	if err := iceberg.WriteManifestList(meta.Version(), &manifestList, 1, nil, &seqNum, 0, manifests); err != nil {
		t.Fatalf("write manifest list: %v", err)
	}
	fs.putEntry(metaDir, "snap-1.avro", &filer_pb.Entry{
		Name: "snap-1.avro", Attributes: &filer_pb.FuseAttributes{Mtime: time.Now().Unix()}, Content: manifestList.Bytes(),
	})

	result, _, err := NewHandler(nil).rewriteManifests(context.Background(), client, setup.BucketName, setup.tablePath(), Config{
		MinManifestsToRewrite: 3,
		MaxCommitRetries:      3,
	})
	if err != nil {
		t.Fatalf("rewriteManifests failed for foreign day partitions: %v", err)
	}
	if result != "rewrote 3 manifests into 1 (3 entries)" {
		t.Fatalf("rewriteManifests result = %q, want 3 manifests merged", result)
	}

	// The merged manifest has to carry the days the foreign manifests held.
	// iceberg-go writes a day partition as a bare Avro int, without the date
	// logical type, so it reads back as int32 rather than iceberg.Date.
	got := make(map[string]any)
	for _, mf := range currentManifests(t, client, setup) {
		manifestData, err := loadFileByIcebergPath(context.Background(), client, setup.BucketName, setup.tablePath(), mf.FilePath())
		if err != nil {
			t.Fatalf("load merged manifest %s: %v", mf.FilePath(), err)
		}
		entries, err := iceberg.ReadManifest(mf, bytes.NewReader(manifestData), true)
		if err != nil {
			t.Fatalf("parse merged manifest %s: %v", mf.FilePath(), err)
		}
		for _, entry := range entries {
			got[entry.DataFile().FilePath()] = entry.DataFile().Partition()[1000]
		}
	}
	if len(got) != len(wantDays) {
		t.Fatalf("merged manifests hold %d entries, want %d", len(got), len(wantDays))
	}
	for filePath, day := range wantDays {
		if got[filePath] != int32(day) {
			t.Errorf("%s partition = %#v (%T), want %d", filePath, got[filePath], got[filePath], day)
		}
	}
}

// currentManifests reads the manifests of the table's current snapshot.
func currentManifests(t *testing.T, client filer_pb.SeaweedFilerClient, setup tableSetup) []iceberg.ManifestFile {
	t.Helper()
	state, err := loadCurrentMetadata(context.Background(), client, setup.BucketName, setup.tablePath())
	if err != nil {
		t.Fatalf("reload metadata: %v", err)
	}
	snapshot := state.Metadata.CurrentSnapshot()
	if snapshot == nil {
		t.Fatal("table has no current snapshot")
	}
	manifestListData, err := loadFileByIcebergPath(context.Background(), client, setup.BucketName, setup.tablePath(), snapshot.ManifestList)
	if err != nil {
		t.Fatalf("load manifest list: %v", err)
	}
	manifests, err := s3tables.ReadManifestList(manifestListData)
	if err != nil {
		t.Fatalf("parse manifest list: %v", err)
	}
	return manifests
}
