// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package iceberg_test

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/apache/iceberg-go"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	tableSchemaNested = iceberg.NewSchemaWithIdentifiers(1,
		[]int{1},
		iceberg.NestedField{
			ID: 1, Name: "foo", Type: iceberg.PrimitiveTypes.String, Required: false,
		},
		iceberg.NestedField{
			ID: 2, Name: "bar", Type: iceberg.PrimitiveTypes.Int32, Required: true,
		},
		iceberg.NestedField{
			ID: 3, Name: "baz", Type: iceberg.PrimitiveTypes.Bool, Required: false,
		},
		iceberg.NestedField{
			ID: 4, Name: "qux", Required: true, Type: &iceberg.ListType{
				ElementID: 5, Element: iceberg.PrimitiveTypes.String, ElementRequired: true,
			},
		},
		iceberg.NestedField{
			ID: 6, Name: "quux",
			Type: &iceberg.MapType{
				KeyID:   7,
				KeyType: iceberg.PrimitiveTypes.String,
				ValueID: 8,
				ValueType: &iceberg.MapType{
					KeyID:         9,
					KeyType:       iceberg.PrimitiveTypes.String,
					ValueID:       10,
					ValueType:     iceberg.PrimitiveTypes.Int32,
					ValueRequired: true,
				},
				ValueRequired: true,
			},
			Required: true,
		},
		iceberg.NestedField{
			ID: 11, Name: "location", Type: &iceberg.ListType{
				ElementID: 12, Element: &iceberg.StructType{
					FieldList: []iceberg.NestedField{
						{ID: 13, Name: "latitude", Type: iceberg.PrimitiveTypes.Float32, Required: false},
						{ID: 14, Name: "longitude", Type: iceberg.PrimitiveTypes.Float32, Required: false},
					},
				},
				ElementRequired: true,
			},
			Required: true,
		},
		iceberg.NestedField{
			ID:   15,
			Name: "person",
			Type: &iceberg.StructType{
				FieldList: []iceberg.NestedField{
					{ID: 16, Name: "name", Type: iceberg.PrimitiveTypes.String, Required: false},
					{ID: 17, Name: "age", Type: iceberg.PrimitiveTypes.Int32, Required: true},
				},
			},
			Required: false,
		},
		iceberg.NestedField{
			ID:   18,
			Name: "thing",
			Type: &iceberg.MapType{
				KeyID:         19,
				KeyType:       iceberg.PrimitiveTypes.String,
				ValueID:       20,
				ValueType:     iceberg.PrimitiveTypes.Int32,
				ValueRequired: true,
			},
			Required: false,
		},
	)

	tableSchemaSimple = iceberg.NewSchemaWithIdentifiers(1,
		[]int{2},
		iceberg.NestedField{ID: 1, Name: "foo", Type: iceberg.PrimitiveTypes.String},
		iceberg.NestedField{ID: 2, Name: "bar", Type: iceberg.PrimitiveTypes.Int32, Required: true},
		iceberg.NestedField{ID: 3, Name: "baz", Type: iceberg.PrimitiveTypes.Bool},
	)
)

func TestSchemaFromJson(t *testing.T) {
	testFieldsStr := `[
		{"id":1,"name":"foo","type":"string","required":false},
		{"id":2,"name":"bar","type":"int","required":true},
		{"id":3,"name":"baz","type":"boolean","required":false}
	]`

	simpleSchema, err := iceberg.NewSchemaFromJsonFields(1, testFieldsStr)
	assert.NoError(t, err)
	assert.Equal(t, tableSchemaSimple.Fields(), simpleSchema.Fields())

	testFieldsStr = `[
		{"id":1,"name":"id","type":"int","required":true,"doc":"primary key"},
		{"id":2,"name":"name","type":"string","required":false,"doc":"user name"}
	]`
	commentSchema, err := iceberg.NewSchemaFromJsonFields(1, testFieldsStr)
	assert.NoError(t, err)
	assert.Equal(t, "primary key", commentSchema.Fields()[0].Doc)
	assert.Equal(t, "user name", commentSchema.Fields()[1].Doc)

	testFieldsStr = `[{
		"id": 1,
		"name": "location",
		"type": {
			"type": "struct",
			"fields": [
				{"id": 11, "name": "address", "type": "string", "required": true},
				{"id": 12, "name": "zip", "type": "int", "required": false}
			]
		},
		"required": true
	}]`
	nestedSchema, err := iceberg.NewSchemaFromJsonFields(1, testFieldsStr)
	assert.NoError(t, err)
	assert.Len(t, nestedSchema.Fields(), 1)

	structType, ok := nestedSchema.Fields()[0].Type.(*iceberg.StructType)
	assert.True(t, ok)
	assert.Len(t, structType.Fields(), 2)
	assert.Equal(t, "address", structType.Fields()[0].Name)
	assert.Equal(t, iceberg.PrimitiveTypes.String, structType.Fields()[0].Type)
	assert.Equal(t, "zip", structType.Fields()[1].Name)
	assert.Equal(t, iceberg.PrimitiveTypes.Int32, structType.Fields()[1].Type)
}

func TestSchemaToString(t *testing.T) {
	assert.Equal(t, 3, tableSchemaSimple.NumFields())
	assert.Equal(t, `table {
	1: foo: optional string
	2: bar: required int
	3: baz: optional boolean
}`, tableSchemaSimple.String())
}

func TestNestedFieldToString(t *testing.T) {
	tests := []struct {
		idx      int
		expected string
	}{
		{0, "1: foo: optional string"},
		{1, "2: bar: required int"},
		{2, "3: baz: optional boolean"},
		{3, "4: qux: required list<string>"},
		{4, "6: quux: required map<string, map<string, int>>"},
		{5, "11: location: required list<struct<13: latitude: optional float, 14: longitude: optional float>>"},
		{6, "15: person: optional struct<16: name: optional string, 17: age: required int>"},
	}

	for _, tt := range tests {
		assert.Equal(t, tt.expected, tableSchemaNested.Field(tt.idx).String())
	}
}

func TestSchemaIndexByIDVisitor(t *testing.T) {
	index, err := iceberg.IndexByID(tableSchemaNested)
	require.NoError(t, err)

	assert.Equal(t, map[int]iceberg.NestedField{
		1: tableSchemaNested.Field(0),
		2: tableSchemaNested.Field(1),
		3: tableSchemaNested.Field(2),
		4: tableSchemaNested.Field(3),
		5: {ID: 5, Name: "element", Type: iceberg.PrimitiveTypes.String, Required: true},
		6: tableSchemaNested.Field(4),
		7: {ID: 7, Name: "key", Type: iceberg.PrimitiveTypes.String, Required: true},
		8: {ID: 8, Name: "value", Type: &iceberg.MapType{
			KeyID:         9,
			KeyType:       iceberg.PrimitiveTypes.String,
			ValueID:       10,
			ValueType:     iceberg.PrimitiveTypes.Int32,
			ValueRequired: true,
		}, Required: true},
		9:  {ID: 9, Name: "key", Type: iceberg.PrimitiveTypes.String, Required: true},
		10: {ID: 10, Name: "value", Type: iceberg.PrimitiveTypes.Int32, Required: true},
		11: tableSchemaNested.Field(5),
		12: {ID: 12, Name: "element", Type: &iceberg.StructType{
			FieldList: []iceberg.NestedField{
				{ID: 13, Name: "latitude", Type: iceberg.PrimitiveTypes.Float32, Required: false},
				{ID: 14, Name: "longitude", Type: iceberg.PrimitiveTypes.Float32, Required: false},
			},
		}, Required: true},
		13: {ID: 13, Name: "latitude", Type: iceberg.PrimitiveTypes.Float32, Required: false},
		14: {ID: 14, Name: "longitude", Type: iceberg.PrimitiveTypes.Float32, Required: false},
		15: tableSchemaNested.Field(6),
		16: {ID: 16, Name: "name", Type: iceberg.PrimitiveTypes.String, Required: false},
		17: {ID: 17, Name: "age", Type: iceberg.PrimitiveTypes.Int32, Required: true},
		18: {ID: 18, Name: "thing", Type: &iceberg.MapType{
			KeyID:         19,
			KeyType:       iceberg.PrimitiveTypes.String,
			ValueID:       20,
			ValueType:     iceberg.PrimitiveTypes.Int32,
			ValueRequired: true,
		}, Required: false},
		19: {ID: 19, Name: "key", Type: iceberg.PrimitiveTypes.String, Required: true},
		20: {ID: 20, Name: "value", Type: iceberg.PrimitiveTypes.Int32, Required: true},
	}, index)
}

func TestSchemaIndexByName(t *testing.T) {
	index, err := iceberg.IndexByName(tableSchemaNested)
	require.NoError(t, err)

	assert.Equal(t, map[string]int{
		"foo":                        1,
		"bar":                        2,
		"baz":                        3,
		"qux":                        4,
		"qux.element":                5,
		"quux":                       6,
		"quux.key":                   7,
		"quux.value":                 8,
		"quux.value.key":             9,
		"quux.value.value":           10,
		"location":                   11,
		"location.element":           12,
		"location.element.latitude":  13,
		"location.element.longitude": 14,
		"location.latitude":          13,
		"location.longitude":         14,
		"person":                     15,
		"person.name":                16,
		"person.age":                 17,
		"thing":                      18,
		"thing.key":                  19,
		"thing.value":                20,
	}, index)
}

func TestSchemaFindColumnName(t *testing.T) {
	tests := []struct {
		id   int
		name string
	}{
		{1, "foo"},
		{2, "bar"},
		{3, "baz"},
		{4, "qux"},
		{5, "qux.element"},
		{6, "quux"},
		{7, "quux.key"},
		{8, "quux.value"},
		{9, "quux.value.key"},
		{10, "quux.value.value"},
		{11, "location"},
		{12, "location.element"},
		{13, "location.element.latitude"},
		{14, "location.element.longitude"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			n, ok := tableSchemaNested.FindColumnName(tt.id)
			assert.True(t, ok)
			assert.Equal(t, tt.name, n)
		})
	}
}

func TestSchemaFindColumnNameIDNotFound(t *testing.T) {
	n, ok := tableSchemaNested.FindColumnName(99)
	assert.False(t, ok)
	assert.Empty(t, n)
}

func TestSchemaFindColumnNameByID(t *testing.T) {
	tests := []struct {
		id   int
		name string
	}{
		{1, "foo"},
		{2, "bar"},
		{3, "baz"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			n, ok := tableSchemaSimple.FindColumnName(tt.id)
			assert.True(t, ok)
			assert.Equal(t, tt.name, n)
		})
	}
}

func TestSchemaFindFieldByID(t *testing.T) {
	index, err := iceberg.IndexByID(tableSchemaSimple)
	require.NoError(t, err)

	col1 := index[1]
	assert.Equal(t, 1, col1.ID)
	assert.Equal(t, iceberg.PrimitiveTypes.String, col1.Type)
	assert.False(t, col1.Required)

	col2 := index[2]
	assert.Equal(t, 2, col2.ID)
	assert.Equal(t, iceberg.PrimitiveTypes.Int32, col2.Type)
	assert.True(t, col2.Required)

	col3 := index[3]
	assert.Equal(t, 3, col3.ID)
	assert.Equal(t, iceberg.PrimitiveTypes.Bool, col3.Type)
	assert.False(t, col3.Required)
}

func TestFindFieldByIDUnknownField(t *testing.T) {
	index, err := iceberg.IndexByID(tableSchemaSimple)
	require.NoError(t, err)
	_, ok := index[4]
	assert.False(t, ok)
}

func TestSchemaFindField(t *testing.T) {
	tests := []iceberg.NestedField{
		{ID: 1, Name: "foo", Type: iceberg.PrimitiveTypes.String, Required: false},
		{ID: 2, Name: "bar", Type: iceberg.PrimitiveTypes.Int32, Required: true},
		{ID: 3, Name: "baz", Type: iceberg.PrimitiveTypes.Bool, Required: false},
	}

	for _, tt := range tests {
		t.Run(tt.Name, func(t *testing.T) {
			f, ok := tableSchemaSimple.FindFieldByID(tt.ID)
			assert.True(t, ok)
			assert.Equal(t, tt, f)

			f, ok = tableSchemaSimple.FindFieldByName(tt.Name)
			assert.True(t, ok)
			assert.Equal(t, tt, f)

			f, ok = tableSchemaSimple.FindFieldByNameCaseInsensitive(strings.ToUpper(tt.Name))
			assert.True(t, ok)
			assert.Equal(t, tt, f)
		})
	}
}

func TestSchemaFindType(t *testing.T) {
	_, ok := tableSchemaSimple.FindTypeByID(0)
	assert.False(t, ok)
	_, ok = tableSchemaSimple.FindTypeByName("FOOBAR")
	assert.False(t, ok)
	_, ok = tableSchemaSimple.FindTypeByNameCaseInsensitive("FOOBAR")
	assert.False(t, ok)

	tests := []struct {
		id   int
		name string
		typ  iceberg.Type
	}{
		{1, "foo", iceberg.PrimitiveTypes.String},
		{2, "bar", iceberg.PrimitiveTypes.Int32},
		{3, "baz", iceberg.PrimitiveTypes.Bool},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			typ, ok := tableSchemaSimple.FindTypeByID(tt.id)
			assert.True(t, ok)
			assert.Equal(t, tt.typ, typ)

			typ, ok = tableSchemaSimple.FindTypeByName(tt.name)
			assert.True(t, ok)
			assert.Equal(t, tt.typ, typ)

			typ, ok = tableSchemaSimple.FindTypeByNameCaseInsensitive(strings.ToUpper(tt.name))
			assert.True(t, ok)
			assert.Equal(t, tt.typ, typ)
		})
	}
}

func TestSerializeSchema(t *testing.T) {
	data, err := json.Marshal(tableSchemaSimple)
	require.NoError(t, err)

	assert.JSONEq(t, `{
		"type": "struct",
		"fields": [
			{"id": 1, "name": "foo", "type": "string", "required": false},
			{"id": 2, "name": "bar", "type": "int", "required": true},
			{"id": 3, "name": "baz", "type": "boolean", "required": false}
		],
		"schema-id": 1,
		"identifier-field-ids": [2]
	}`, string(data))
}

func TestUnmarshalSchema(t *testing.T) {
	var schema iceberg.Schema
	require.NoError(t, json.Unmarshal([]byte(`{
		"type": "struct",
		"fields": [
			{"id": 1, "name": "foo", "type": "string", "required": false},
			{"id": 2, "name": "bar", "type": "int", "required": true},
			{"id": 3, "name": "baz", "type": "boolean", "required": false}
		],
		"schema-id": 1,
		"identifier-field-ids": [2]
	}`), &schema))

	assert.True(t, tableSchemaSimple.Equals(&schema))
}

func TestPruneColumnsString(t *testing.T) {
	sc, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{1: {}}, false)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchemaWithIdentifiers(1, []int{1},
		iceberg.NestedField{ID: 1, Name: "foo", Type: iceberg.PrimitiveTypes.String, Required: false})))
}

func TestPruneColumnsStringFull(t *testing.T) {
	sc, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{1: {}}, true)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchemaWithIdentifiers(1, []int{1},
		iceberg.NestedField{ID: 1, Name: "foo", Type: iceberg.PrimitiveTypes.String, Required: false})))
}

func TestPruneColumnsList(t *testing.T) {
	sc, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{5: {}}, false)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchema(1,
		iceberg.NestedField{ID: 4, Name: "qux", Required: true, Type: &iceberg.ListType{
			ElementID: 5, Element: iceberg.PrimitiveTypes.String, ElementRequired: true,
		}})))
}

func TestPruneColumnsListItself(t *testing.T) {
	_, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{4: {}}, false)
	assert.ErrorIs(t, err, iceberg.ErrInvalidSchema)

	assert.ErrorContains(t, err, "cannot explicitly project List or Map types, 4:qux of type list<string> was selected")
}

func TestPruneColumnsListFull(t *testing.T) {
	sc, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{5: {}}, true)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchema(1,
		iceberg.NestedField{ID: 4, Name: "qux", Required: true, Type: &iceberg.ListType{
			ElementID: 5, Element: iceberg.PrimitiveTypes.String, ElementRequired: true,
		}})))
}

func TestPruneColumnsMap(t *testing.T) {
	sc, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{9: {}}, false)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchema(1,
		iceberg.NestedField{
			ID:       6,
			Name:     "quux",
			Required: true,
			Type: &iceberg.MapType{
				KeyID:   7,
				KeyType: iceberg.PrimitiveTypes.String,
				ValueID: 8,
				ValueType: &iceberg.MapType{
					KeyID:         9,
					KeyType:       iceberg.PrimitiveTypes.String,
					ValueID:       10,
					ValueType:     iceberg.PrimitiveTypes.Int32,
					ValueRequired: true,
				},
				ValueRequired: true,
			},
		})))
}

func TestPruneColumnsMapItself(t *testing.T) {
	_, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{6: {}}, false)
	assert.ErrorIs(t, err, iceberg.ErrInvalidSchema)
	assert.ErrorContains(t, err, "cannot explicitly project List or Map types, 6:quux of type map<string, map<string, int>> was selected")
}

func TestPruneColumnsMapFull(t *testing.T) {
	sc, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{9: {}}, true)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchema(1,
		iceberg.NestedField{
			ID:       6,
			Name:     "quux",
			Required: true,
			Type: &iceberg.MapType{
				KeyID:   7,
				KeyType: iceberg.PrimitiveTypes.String,
				ValueID: 8,
				ValueType: &iceberg.MapType{
					KeyID:         9,
					KeyType:       iceberg.PrimitiveTypes.String,
					ValueID:       10,
					ValueType:     iceberg.PrimitiveTypes.Int32,
					ValueRequired: true,
				},
				ValueRequired: true,
			},
		})))
}

func TestPruneColumnsMapKey(t *testing.T) {
	sc, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{10: {}}, false)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchema(1,
		iceberg.NestedField{
			ID:       6,
			Name:     "quux",
			Required: true,
			Type: &iceberg.MapType{
				KeyID:   7,
				KeyType: iceberg.PrimitiveTypes.String,
				ValueID: 8,
				ValueType: &iceberg.MapType{
					KeyID:         9,
					KeyType:       iceberg.PrimitiveTypes.String,
					ValueID:       10,
					ValueType:     iceberg.PrimitiveTypes.Int32,
					ValueRequired: true,
				},
				ValueRequired: true,
			},
		})))
}

func TestPruneColumnsStruct(t *testing.T) {
	sc, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{16: {}}, false)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchema(1,
		iceberg.NestedField{
			ID:       15,
			Name:     "person",
			Required: false,
			Type: &iceberg.StructType{
				FieldList: []iceberg.NestedField{{
					ID: 16, Name: "name", Type: iceberg.PrimitiveTypes.String, Required: false,
				}},
			},
		})))
}

func TestPruneColumnsStructFull(t *testing.T) {
	sc, err := iceberg.PruneColumns(tableSchemaNested, map[int]iceberg.Void{16: {}}, true)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchema(1,
		iceberg.NestedField{
			ID:       15,
			Name:     "person",
			Required: false,
			Type: &iceberg.StructType{
				FieldList: []iceberg.NestedField{{
					ID: 16, Name: "name", Type: iceberg.PrimitiveTypes.String, Required: false,
				}},
			},
		})))
}

func TestPruneColumnsEmptyStruct(t *testing.T) {
	schemaEmptyStruct := iceberg.NewSchema(0, iceberg.NestedField{
		ID: 15, Name: "person", Type: &iceberg.StructType{}, Required: false,
	})

	sc, err := iceberg.PruneColumns(schemaEmptyStruct, map[int]iceberg.Void{15: {}}, false)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchema(0,
		iceberg.NestedField{
			ID: 15, Name: "person", Type: &iceberg.StructType{}, Required: false,
		})))
}

func TestPruneColumnsEmptyStructFull(t *testing.T) {
	schemaEmptyStruct := iceberg.NewSchema(0, iceberg.NestedField{
		ID: 15, Name: "person", Type: &iceberg.StructType{}, Required: false,
	})

	sc, err := iceberg.PruneColumns(schemaEmptyStruct, map[int]iceberg.Void{15: {}}, true)
	require.NoError(t, err)

	assert.True(t, sc.Equals(iceberg.NewSchema(0,
		iceberg.NestedField{
			ID: 15, Name: "person", Type: &iceberg.StructType{}, Required: false,
		})))
}

func TestPruneColumnsStructInMap(t *testing.T) {
	nestedSchema := iceberg.NewSchemaWithIdentifiers(1, []int{1},
		iceberg.NestedField{
			ID:       6,
			Name:     "id_to_person",
			Required: true,
			Type: &iceberg.MapType{
				KeyID:   7,
				KeyType: iceberg.PrimitiveTypes.Int32,
				ValueID: 8,
				ValueType: &iceberg.StructType{
					FieldList: []iceberg.NestedField{
						{ID: 10, Name: "name", Type: iceberg.PrimitiveTypes.String},
						{ID: 11, Name: "age", Type: iceberg.PrimitiveTypes.Int32, Required: true},
					},
				},
				ValueRequired: true,
			},
		})

	sc, err := iceberg.PruneColumns(nestedSchema, map[int]iceberg.Void{11: {}}, false)
	require.NoError(t, err)

	expected := iceberg.NewSchema(1,
		iceberg.NestedField{
			ID:       6,
			Name:     "id_to_person",
			Required: true,
			Type: &iceberg.MapType{
				KeyID:   7,
				KeyType: iceberg.PrimitiveTypes.Int32,
				ValueID: 8,
				ValueType: &iceberg.StructType{
					FieldList: []iceberg.NestedField{
						{ID: 11, Name: "age", Type: iceberg.PrimitiveTypes.Int32, Required: true},
					},
				},
				ValueRequired: true,
			},
		})

	assert.Truef(t, sc.Equals(expected), "expected: %s\ngot: %s", expected, sc)
}

func TestPruneColumnsStructInMapFull(t *testing.T) {
	nestedSchema := iceberg.NewSchemaWithIdentifiers(1, []int{1},
		iceberg.NestedField{
			ID:       6,
			Name:     "id_to_person",
			Required: true,
			Type: &iceberg.MapType{
				KeyID:   7,
				KeyType: iceberg.PrimitiveTypes.Int32,
				ValueID: 8,
				ValueType: &iceberg.StructType{
					FieldList: []iceberg.NestedField{
						{ID: 10, Name: "name", Type: iceberg.PrimitiveTypes.String},
						{ID: 11, Name: "age", Type: iceberg.PrimitiveTypes.Int32, Required: true},
					},
				},
				ValueRequired: true,
			},
		})

	sc, err := iceberg.PruneColumns(nestedSchema, map[int]iceberg.Void{11: {}}, true)
	require.NoError(t, err)

	expected := iceberg.NewSchema(1,
		iceberg.NestedField{
			ID:       6,
			Name:     "id_to_person",
			Required: true,
			Type: &iceberg.MapType{
				KeyID:   7,
				KeyType: iceberg.PrimitiveTypes.Int32,
				ValueID: 8,
				ValueType: &iceberg.StructType{
					FieldList: []iceberg.NestedField{
						{ID: 11, Name: "age", Type: iceberg.PrimitiveTypes.Int32, Required: true},
					},
				},
				ValueRequired: true,
			},
		})

	assert.Truef(t, sc.Equals(expected), "expected: %s\ngot: %s", expected, sc)
}

func TestPruneColumnsSelectOriginalSchema(t *testing.T) {
	id := tableSchemaNested.HighestFieldID()
	selected := make(map[int]iceberg.Void)
	for i := 0; i < id; i++ {
		selected[i] = iceberg.Void{}
	}

	sc, err := iceberg.PruneColumns(tableSchemaNested, selected, true)
	require.NoError(t, err)

	assert.True(t, sc.Equals(tableSchemaNested))
}

func TestPruneNilSchema(t *testing.T) {
	_, err := iceberg.PruneColumns(nil, nil, true)
	assert.ErrorIs(t, err, iceberg.ErrInvalidArgument)
}

func TestAssignFreshSchemaIDs(t *testing.T) {
	startID := 100
	sc, err := iceberg.AssignFreshSchemaIDs(tableSchemaNested, func() int {
		startID++

		return startID
	})
	require.NoError(t, err)
	require.NotNil(t, sc)

	startID = 100
	var checkID func(iceberg.NestedField)
	checkID = func(f iceberg.NestedField) {
		startID++
		assert.Equal(t, startID, f.ID)
		if nested, ok := f.Type.(iceberg.NestedType); ok {
			for _, nf := range nested.Fields() {
				checkID(nf)
			}
		}
	}

	for _, f := range sc.Fields() {
		checkID(f)
	}
}

func TestSchemaRoundTrip(t *testing.T) {
	data, err := json.Marshal(tableSchemaNested)
	require.NoError(t, err)

	assert.JSONEq(t, `{
		"type": "struct",
		"schema-id": 1,
		"identifier-field-ids": [1],
		"fields": [
			{
				"type": "string",
				"id": 1,
				"name": "foo",
				"required": false
			},
			{
				"type": "int",
				"id": 2,
				"name": "bar",
				"required": true
			},
			{
				"type": "boolean",
				"id": 3,
				"name": "baz",
				"required": false
			},
			{
				"id": 4,
				"name": "qux",
				"required": true,
				"type": {
					"type": "list",
					"element-id": 5,
					"element-required": true,
					"element": "string"
				}
			},
			{
				"id": 6,
				"name": "quux",
				"required": true,
				"type": {
					"type": "map",
					"key-id": 7,
					"key": "string",
					"value-id": 8,
					"value": {
						"type": "map",
						"key-id": 9,
						"key": "string",
						"value-id": 10,
						"value": "int",
						"value-required": true
					},
					"value-required": true
				}
			},
			{
				"id": 11,
				"name": "location",
				"required": true,
				"type": {
					"type": "list",
					"element-id": 12,
					"element-required": true,
					"element": {
						"type": "struct",
						"fields": [
							{
								"id": 13,
								"name": "latitude",
								"type": "float",
								"required": false
							},
							{
								"id": 14,
								"name": "longitude",
								"type": "float",
								"required": false
							}
						]
					}
				}
			},
			{
				"id": 15,
				"name": "person",
				"required": false,
				"type": {
					"type": "struct",
					"fields": [
						{
							"id": 16,
							"name": "name",
							"type": "string",
							"required": false
						},
						{
							"id": 17,
							"name": "age",
							"type": "int",
							"required": true
						}
					]
				}
			},
			{
				"id": 18,
				"name": "thing",
				"required": false,
				"type": {
					"type": "map",
					"key-id": 19,
					"key": "string",
					"value-id": 20,
					"value": "int",
					"value-required": true
				}
			}
		]
	}`, string(data))

	var sc iceberg.Schema
	require.NoError(t, json.Unmarshal(data, &sc))

	assert.Truef(t, tableSchemaNested.Equals(&sc), "expected: %s\ngot: %s", tableSchemaNested, &sc)
}

func TestHighestFieldID(t *testing.T) {
	id := tableSchemaNested.HighestFieldID()
	assert.Equal(t, 20, id, "expected highest field ID to be 20, got %d", id)
}
