package builderimp

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/tech/sendico/pkg/db/repository/builder"
	"github.com/tech/sendico/pkg/mservice"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

func TestNewPipelineImp(t *testing.T) {
	pipeline := NewPipelineImp()

	assert.NotNil(t, pipeline)
	assert.IsType(t, &PipelineImp{}, pipeline)

	// Build should return empty pipeline initially
	built := pipeline.Build()
	assert.NotNil(t, built)
	assert.Len(t, built, 0)
}

func TestPipelineImp_Match(t *testing.T) {
	pipeline := NewPipelineImp()
	mockQuery := &MockQuery{
		buildPipeline: bson.D{{Key: "$match", Value: bson.D{{Key: "field", Value: "value"}}}},
	}

	result := pipeline.Match(mockQuery)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 1)
	assert.Equal(t, bson.D{{Key: "$match", Value: bson.D{{Key: "field", Value: "value"}}}}, built[0])
}

func TestPipelineImp_Lookup(t *testing.T) {
	pipeline := NewPipelineImp()
	mockLocalField := &MockField{build: "localField"}
	mockForeignField := &MockField{build: "foreignField"}
	mockAsField := &MockField{build: "asField"}

	result := pipeline.Lookup(mservice.Site, mockLocalField, mockForeignField, mockAsField)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{
		{Key: string(builder.MKFrom), Value: mservice.Site},
		{Key: string(builder.MKLocalField), Value: "localField"},
		{Key: string(builder.MKForeignField), Value: "foreignField"},
		{Key: string(builder.MKAs), Value: "asField"},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_LookupWithPipeline_WithoutLet(t *testing.T) {
	pipeline := NewPipelineImp()
	mockNestedPipeline := &MockPipeline{
		build: mongo.Pipeline{bson.D{{Key: "$match", Value: bson.D{{Key: "nested", Value: true}}}}},
	}
	mockAsField := &MockField{build: "asField"}

	result := pipeline.LookupWithPipeline(mservice.Site, mockNestedPipeline, mockAsField, nil)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{
		{Key: string(builder.MKFrom), Value: mservice.Site},
		{Key: string(builder.MKPipeline), Value: mockNestedPipeline.build},
		{Key: string(builder.MKAs), Value: "asField"},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_LookupWithPipeline_WithLet(t *testing.T) {
	pipeline := NewPipelineImp()
	mockNestedPipeline := &MockPipeline{
		build: mongo.Pipeline{bson.D{{Key: "$match", Value: bson.D{{Key: "nested", Value: true}}}}},
	}
	mockAsField := &MockField{build: "asField"}
	mockLetField := &MockField{build: "$_id"}

	letVars := map[string]builder.Field{
		"projRef": mockLetField,
	}

	result := pipeline.LookupWithPipeline(mservice.Site, mockNestedPipeline, mockAsField, &letVars)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{
		{Key: string(builder.MKFrom), Value: mservice.Site},
		{Key: string(builder.MKPipeline), Value: mockNestedPipeline.build},
		{Key: string(builder.MKAs), Value: "asField"},
		{Key: string(builder.MKLet), Value: bson.D{{Key: "projRef", Value: "$_id"}}},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_LookupWithPipeline_WithEmptyLet(t *testing.T) {
	pipeline := NewPipelineImp()
	mockNestedPipeline := &MockPipeline{
		build: mongo.Pipeline{bson.D{{Key: "$match", Value: bson.D{{Key: "nested", Value: true}}}}},
	}
	mockAsField := &MockField{build: "asField"}

	emptyLetVars := map[string]builder.Field{}

	pipeline.LookupWithPipeline(mservice.Site, mockNestedPipeline, mockAsField, &emptyLetVars)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	// Should not include let field when empty
	expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{
		{Key: string(builder.MKFrom), Value: mservice.Site},
		{Key: string(builder.MKPipeline), Value: mockNestedPipeline.build},
		{Key: string(builder.MKAs), Value: "asField"},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_Unwind_Simple(t *testing.T) {
	pipeline := NewPipelineImp()
	mockField := &MockField{build: "$array"}

	result := pipeline.Unwind(mockField)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Unwind), Value: "$array"}}
	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_Unwind_WithPreserveNullAndEmptyArrays(t *testing.T) {
	pipeline := NewPipelineImp()
	mockField := &MockField{build: "$array"}

	// Mock the UnwindOption function
	preserveOpt := func(opts *builder.UnwindOpts) {
		opts.PreserveNullAndEmptyArrays = true
	}

	pipeline.Unwind(mockField, preserveOpt)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Unwind), Value: bson.D{
		{Key: string(builder.MKPath), Value: "$array"},
		{Key: string(builder.MKPreserveNullAndEmptyArrays), Value: true},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_Unwind_WithIncludeArrayIndex(t *testing.T) {
	pipeline := NewPipelineImp()
	mockField := &MockField{build: "$array"}

	// Mock the UnwindOption function
	indexOpt := func(opts *builder.UnwindOpts) {
		opts.IncludeArrayIndex = "arrayIndex"
	}

	pipeline.Unwind(mockField, indexOpt)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Unwind), Value: bson.D{
		{Key: string(builder.MKPath), Value: "$array"},
		{Key: string(builder.MKIncludeArrayIndex), Value: "arrayIndex"},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_Unwind_WithBothOptions(t *testing.T) {
	pipeline := NewPipelineImp()
	mockField := &MockField{build: "$array"}

	// Mock the UnwindOption functions
	preserveOpt := func(opts *builder.UnwindOpts) {
		opts.PreserveNullAndEmptyArrays = true
	}
	indexOpt := func(opts *builder.UnwindOpts) {
		opts.IncludeArrayIndex = "arrayIndex"
	}

	pipeline.Unwind(mockField, preserveOpt, indexOpt)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Unwind), Value: bson.D{
		{Key: string(builder.MKPath), Value: "$array"},
		{Key: string(builder.MKPreserveNullAndEmptyArrays), Value: true},
		{Key: string(builder.MKIncludeArrayIndex), Value: "arrayIndex"},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_Count(t *testing.T) {
	pipeline := NewPipelineImp()
	mockField := &MockField{build: "totalCount"}

	result := pipeline.Count(mockField)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Count), Value: "totalCount"}}
	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_Group(t *testing.T) {
	pipeline := NewPipelineImp()
	mockAlias := &MockAlias{
		build: bson.D{{Key: "_id", Value: "$field"}},
		field: &MockField{build: "_id"},
	}
	mockAccumulator := &MockGroupAccumulator{
		build: bson.D{{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}},
	}

	result := pipeline.Group(mockAlias, mockAccumulator)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Group), Value: bson.D{
		{Key: "_id", Value: "$field"},
		{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_Group_MultipleAccumulators(t *testing.T) {
	pipeline := NewPipelineImp()
	mockAlias := &MockAlias{
		build: bson.D{{Key: "_id", Value: "$field"}},
		field: &MockField{build: "_id"},
	}
	mockAccumulator1 := &MockGroupAccumulator{
		build: bson.D{{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}},
	}
	mockAccumulator2 := &MockGroupAccumulator{
		build: bson.D{{Key: "total", Value: bson.D{{Key: "$sum", Value: "$amount"}}}},
	}

	pipeline.Group(mockAlias, mockAccumulator1, mockAccumulator2)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Group), Value: bson.D{
		{Key: "_id", Value: "$field"},
		{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}},
		{Key: "total", Value: bson.D{{Key: "$sum", Value: "$amount"}}},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_Project(t *testing.T) {
	pipeline := NewPipelineImp()
	mockProjection := &MockProjection{
		build: bson.D{{Key: "field1", Value: 1}},
	}

	result := pipeline.Project(mockProjection)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Project), Value: bson.D{
		{Key: "field1", Value: 1},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_Project_MultipleProjections(t *testing.T) {
	pipeline := NewPipelineImp()
	mockProjection1 := &MockProjection{
		build: bson.D{{Key: "field1", Value: 1}},
	}
	mockProjection2 := &MockProjection{
		build: bson.D{{Key: "field2", Value: 0}},
	}

	pipeline.Project(mockProjection1, mockProjection2)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.Project), Value: bson.D{
		{Key: "field1", Value: 1},
		{Key: "field2", Value: 0},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_ChainedOperations(t *testing.T) {
	pipeline := NewPipelineImp()

	// Create mocks
	mockQuery := &MockQuery{
		buildPipeline: bson.D{{Key: "$match", Value: bson.D{{Key: "status", Value: "active"}}}},
	}
	mockLocalField := &MockField{build: "userId"}
	mockForeignField := &MockField{build: "_id"}
	mockAsField := &MockField{build: "user"}
	mockUnwindField := &MockField{build: "$user"}
	mockProjection := &MockProjection{
		build: bson.D{{Key: "name", Value: "$user.name"}},
	}

	// Chain operations
	result := pipeline.
		Match(mockQuery).
		Lookup(mservice.Accounts, mockLocalField, mockForeignField, mockAsField).
		Unwind(mockUnwindField).
		Project(mockProjection)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 4)

	// Verify each stage
	assert.Equal(t, bson.D{{Key: "$match", Value: bson.D{{Key: "status", Value: "active"}}}}, built[0])

	expectedLookup := bson.D{{Key: string(builder.Lookup), Value: bson.D{
		{Key: string(builder.MKFrom), Value: mservice.Accounts},
		{Key: string(builder.MKLocalField), Value: "userId"},
		{Key: string(builder.MKForeignField), Value: "_id"},
		{Key: string(builder.MKAs), Value: "user"},
	}}}
	assert.Equal(t, expectedLookup, built[1])

	assert.Equal(t, bson.D{{Key: string(builder.Unwind), Value: "$user"}}, built[2])

	expectedProject := bson.D{{Key: string(builder.Project), Value: bson.D{
		{Key: "name", Value: "$user.name"},
	}}}
	assert.Equal(t, expectedProject, built[3])
}

func TestNewUnwindOpts(t *testing.T) {
	t.Run("NoOptions", func(t *testing.T) {
		opts := NewUnwindOpts()

		assert.NotNil(t, opts)
		assert.False(t, opts.PreserveNullAndEmptyArrays)
		assert.Empty(t, opts.IncludeArrayIndex)
	})

	t.Run("WithPreserveOption", func(t *testing.T) {
		preserveOpt := func(opts *builder.UnwindOpts) {
			opts.PreserveNullAndEmptyArrays = true
		}

		opts := NewUnwindOpts(preserveOpt)

		assert.True(t, opts.PreserveNullAndEmptyArrays)
		assert.Empty(t, opts.IncludeArrayIndex)
	})

	t.Run("WithIndexOption", func(t *testing.T) {
		indexOpt := func(opts *builder.UnwindOpts) {
			opts.IncludeArrayIndex = "index"
		}

		opts := NewUnwindOpts(indexOpt)

		assert.False(t, opts.PreserveNullAndEmptyArrays)
		assert.Equal(t, "index", opts.IncludeArrayIndex)
	})

	t.Run("WithBothOptions", func(t *testing.T) {
		preserveOpt := func(opts *builder.UnwindOpts) {
			opts.PreserveNullAndEmptyArrays = true
		}
		indexOpt := func(opts *builder.UnwindOpts) {
			opts.IncludeArrayIndex = "index"
		}

		opts := NewUnwindOpts(preserveOpt, indexOpt)

		assert.True(t, opts.PreserveNullAndEmptyArrays)
		assert.Equal(t, "index", opts.IncludeArrayIndex)
	})
}

// Mock implementations for testing

type MockQuery struct {
	buildPipeline bson.D
}

func (m *MockQuery) And(filters ...builder.Query) builder.Query          { return m }
func (m *MockQuery) Or(filters ...builder.Query) builder.Query           { return m }
func (m *MockQuery) Filter(field builder.Field, value any) builder.Query { return m }
func (m *MockQuery) Expression(value builder.Expression) builder.Query   { return m }
func (m *MockQuery) Comparison(field builder.Field, operator builder.MongoOperation, value any) builder.Query {
	return m
}
func (m *MockQuery) RegEx(field builder.Field, pattern, options string) builder.Query { return m }
func (m *MockQuery) In(field builder.Field, values ...any) builder.Query              { return m }
func (m *MockQuery) NotIn(field builder.Field, values ...any) builder.Query           { return m }
func (m *MockQuery) Sort(field builder.Field, ascending bool) builder.Query           { return m }
func (m *MockQuery) Limit(limit *int64) builder.Query                                 { return m }
func (m *MockQuery) Offset(offset *int64) builder.Query                               { return m }
func (m *MockQuery) Archived(isArchived *bool) builder.Query                          { return m }
func (m *MockQuery) BuildPipeline() bson.D                                            { return m.buildPipeline }
func (m *MockQuery) BuildQuery() bson.D                                               { return bson.D{} }
func (m *MockQuery) BuildOptions() *options.FindOptionsBuilder                        { return &options.FindOptionsBuilder{} }

type MockField struct {
	build string
}

func (m *MockField) Dot(field string) builder.Field      { return &MockField{build: m.build + "." + field} }
func (m *MockField) CopyWith(field string) builder.Field { return &MockField{build: field} }
func (m *MockField) Build() string                       { return m.build }

type MockPipeline struct {
	build mongo.Pipeline
}

func (m *MockPipeline) Match(filter builder.Query) builder.Pipeline { return m }
func (m *MockPipeline) Lookup(from mservice.Type, localField, foreignField, as builder.Field) builder.Pipeline {
	return m
}
func (m *MockPipeline) LookupWithPipeline(from mservice.Type, pipeline builder.Pipeline, as builder.Field, let *map[string]builder.Field) builder.Pipeline {
	return m
}
func (m *MockPipeline) Unwind(path builder.Field, opts ...UnwindOption) builder.Pipeline { return m }
func (m *MockPipeline) Count(field builder.Field) builder.Pipeline                       { return m }
func (m *MockPipeline) Group(groupBy builder.Alias, accumulators ...builder.GroupAccumulator) builder.Pipeline {
	return m
}
func (m *MockPipeline) Project(projections ...builder.Projection) builder.Pipeline { return m }
func (m *MockPipeline) ReplaceRoot(newRoot builder.Expression) builder.Pipeline    { return m }
func (m *MockPipeline) Build() mongo.Pipeline                                      { return m.build }

type MockAlias struct {
	build bson.D
	field builder.Field
}

func (m *MockAlias) Field() builder.Field { return m.field }
func (m *MockAlias) Build() bson.D        { return m.build }

type MockGroupAccumulator struct {
	build bson.D
}

func (m *MockGroupAccumulator) Build() bson.D { return m.build }

type MockProjection struct {
	build bson.D
}

func (m *MockProjection) Build() bson.D { return m.build }

func TestPipelineImp_ReplaceRoot(t *testing.T) {
	pipeline := NewPipelineImp()
	mockExpr := &MockExpression{build: "$newRoot"}

	result := pipeline.ReplaceRoot(mockExpr)

	// Should return self for chaining
	assert.Same(t, pipeline, result)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.ReplaceRoot), Value: bson.D{
		{Key: string(builder.MKNewRoot), Value: "$newRoot"},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_ReplaceRoot_WithNestedField(t *testing.T) {
	pipeline := NewPipelineImp()
	mockExpr := &MockExpression{build: "$document.data"}

	pipeline.ReplaceRoot(mockExpr)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.ReplaceRoot), Value: bson.D{
		{Key: string(builder.MKNewRoot), Value: "$document.data"},
	}}}

	assert.Equal(t, expected, built[0])
}

func TestPipelineImp_ReplaceRoot_WithExpression(t *testing.T) {
	pipeline := NewPipelineImp()
	// Mock a complex expression like { $mergeObjects: [...] }
	mockExpr := &MockExpression{build: bson.D{{Key: "$mergeObjects", Value: bson.A{"$field1", "$field2"}}}}

	pipeline.ReplaceRoot(mockExpr)

	built := pipeline.Build()
	assert.Len(t, built, 1)

	expected := bson.D{{Key: string(builder.ReplaceRoot), Value: bson.D{
		{Key: string(builder.MKNewRoot), Value: bson.D{{Key: "$mergeObjects", Value: bson.A{"$field1", "$field2"}}}},
	}}}

	assert.Equal(t, expected, built[0])
}

type MockExpression struct {
	build any
}

func (m *MockExpression) Build() any { return m.build }
