package sql_test

import (
	"context"
	"errors"
	"testing"
	"tercul/internal/data/sql"
	"tercul/internal/domain"
	"tercul/internal/platform/config"
	"tercul/internal/testutil"

	"github.com/stretchr/testify/suite"
	"gorm.io/gorm"
)

// BaseRepositoryTestSuite tests the generic BaseRepository implementation.
type BaseRepositoryTestSuite struct {
	testutil.IntegrationTestSuite
	repo domain.BaseRepository[testutil.TestEntity]
	cfg  *config.Config
}

// SetupSuite initializes the test suite, database, and repository.
func (s *BaseRepositoryTestSuite) SetupSuite() {
	s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
	cfg, err := config.LoadConfig()
	s.Require().NoError(err)
	s.cfg = cfg
	s.repo = sql.NewBaseRepositoryImpl[testutil.TestEntity](s.DB, s.cfg)
}

// SetupTest cleans the database before each test.
func (s *BaseRepositoryTestSuite) SetupTest() {
	s.IntegrationTestSuite.SetupTest()
	s.DB.Exec("DELETE FROM test_entities")
}

// TearDownSuite drops the test table after the suite finishes.
func (s *BaseRepositoryTestSuite) TearDownSuite() {
	err := s.DB.Migrator().DropTable(&testutil.TestEntity{})
	s.Require().NoError(err)
}

// TestBaseRepository runs the entire test suite.
func TestBaseRepository(t *testing.T) {
	suite.Run(t, new(BaseRepositoryTestSuite))
}

// createTestEntity is a helper to create a test entity.
func (s *BaseRepositoryTestSuite) createTestEntity(name string) *testutil.TestEntity {
	entity := &testutil.TestEntity{Name: name}
	err := s.repo.Create(context.Background(), entity)
	s.Require().NoError(err)
	s.Require().NotZero(entity.ID)
	return entity
}

func (s *BaseRepositoryTestSuite) TestCreate() {
	s.Run("should create a new entity", func() {
		// Arrange
		ctx := context.Background()
		entity := &testutil.TestEntity{Name: "Test Create"}

		// Act
		err := s.repo.Create(ctx, entity)

		// Assert
		s.Require().NoError(err)
		s.NotZero(entity.ID)

		// Verify in DB
		var foundEntity testutil.TestEntity
		err = s.DB.First(&foundEntity, entity.ID).Error
		s.Require().NoError(err)
		s.Equal("Test Create", foundEntity.Name)
	})

	s.Run("should return error for nil entity", func() {
		err := s.repo.Create(context.Background(), nil)
		s.ErrorIs(err, domain.ErrValidation)
	})

	s.Run("should return error for nil context", func() {
		//nolint:staticcheck // Testing behavior with nil context is intentional here.
		err := s.repo.Create(nil, &testutil.TestEntity{Name: "Test Context"})
		s.ErrorIs(err, domain.ErrValidation)
	})
}

func (s *BaseRepositoryTestSuite) TestGetByID() {
	s.Run("should return an entity by ID", func() {
		// Arrange
		created := s.createTestEntity("Test GetByID")

		// Act
		found, err := s.repo.GetByID(context.Background(), created.ID)

		// Assert
		s.Require().NoError(err)
		s.Require().NotNil(found)
		s.Equal(created.ID, found.ID)
		s.Equal(created.Name, found.Name)
	})

	s.Run("should return ErrEntityNotFound for non-existent ID", func() {
		_, err := s.repo.GetByID(context.Background(), 99999)
		s.ErrorIs(err, domain.ErrEntityNotFound)
	})

	s.Run("should return ErrValidation for zero ID", func() {
		_, err := s.repo.GetByID(context.Background(), 0)
		s.ErrorIs(err, domain.ErrValidation)
	})
}

func (s *BaseRepositoryTestSuite) TestUpdate() {
	s.Run("should update an existing entity", func() {
		// Arrange
		created := s.createTestEntity("Original Name")
		created.Name = "Updated Name"

		// Act
		err := s.repo.Update(context.Background(), created)

		// Assert
		s.Require().NoError(err)
		found, getErr := s.repo.GetByID(context.Background(), created.ID)
		s.Require().NoError(getErr)
		s.Equal("Updated Name", found.Name)
	})
}

func (s *BaseRepositoryTestSuite) TestDelete() {
	s.Run("should delete an existing entity", func() {
		// Arrange
		created := s.createTestEntity("To Be Deleted")

		// Act
		err := s.repo.Delete(context.Background(), created.ID)

		// Assert
		s.Require().NoError(err)
		_, getErr := s.repo.GetByID(context.Background(), created.ID)
		s.ErrorIs(getErr, domain.ErrEntityNotFound)
	})

	s.Run("should return ErrEntityNotFound when deleting non-existent entity", func() {
		err := s.repo.Delete(context.Background(), 99999)
		s.ErrorIs(err, domain.ErrEntityNotFound)
	})
}

func (s *BaseRepositoryTestSuite) TestList() {
	// Arrange
	s.createTestEntity("Entity 1")
	s.createTestEntity("Entity 2")
	s.createTestEntity("Entity 3")

	s.Run("should return a paginated list of entities", func() {
		// Act
		result, err := s.repo.List(context.Background(), 1, 2)

		// Assert
		s.Require().NoError(err)
		s.Equal(int64(3), result.TotalCount)
		s.Equal(2, result.TotalPages)
		s.Equal(1, result.Page)
		s.Equal(2, result.PageSize)
		s.True(result.HasNext)
		s.False(result.HasPrev)
		s.Len(result.Items, 2)
	})
}

func (s *BaseRepositoryTestSuite) TestListWithOptions() {
	// Arrange
	s.createTestEntity("Apple")
	s.createTestEntity("Banana")
	s.createTestEntity("Avocado")

	s.Run("should filter with Where clause", func() {
		// Act
		options := &domain.QueryOptions{
			Where: map[string]interface{}{"name LIKE ?": "A%"},
		}
		results, err := s.repo.ListWithOptions(context.Background(), options)

		// Assert
		s.Require().NoError(err)
		s.Len(results, 2)
	})

	s.Run("should order results", func() {
		// Act
		options := &domain.QueryOptions{OrderBy: "name desc"}
		results, err := s.repo.ListWithOptions(context.Background(), options)

		// Assert
		s.Require().NoError(err)
		s.Len(results, 3)
		s.Equal("Banana", results[0].Name)
		s.Equal("Avocado", results[1].Name)
		s.Equal("Apple", results[2].Name)
	})
}

func (s *BaseRepositoryTestSuite) TestCount() {
	// Arrange
	s.createTestEntity("Entity 1")
	s.createTestEntity("Entity 2")

	s.Run("should return the total count of entities", func() {
		// Act
		count, err := s.repo.Count(context.Background())

		// Assert
		s.Require().NoError(err)
		s.Equal(int64(2), count)
	})
}

func (s *BaseRepositoryTestSuite) TestWithTx() {
	s.Run("should commit transaction on success", func() {
		// Arrange
		var createdID uint

		// Act
		err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
			entity := &testutil.TestEntity{Name: "TX Commit"}
			repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg)
			if err := repoInTx.Create(context.Background(), entity); err != nil {
				return err
			}
			createdID = entity.ID
			return nil
		})

		// Assert
		s.Require().NoError(err)
		_, getErr := s.repo.GetByID(context.Background(), createdID)
		s.NoError(getErr, "Entity should exist after commit")
	})

	s.Run("should rollback transaction on error", func() {
		// Arrange
		var createdID uint
		simulatedErr := errors.New("simulated error")

		// Act
		err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
			entity := &testutil.TestEntity{Name: "TX Rollback"}
			repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg)
			if err := repoInTx.Create(context.Background(), entity); err != nil {
				return err
			}
			createdID = entity.ID
			return simulatedErr // Force a rollback
		})

		// Assert
		s.Require().Error(err)
		s.ErrorIs(err, simulatedErr)

		_, getErr := s.repo.GetByID(context.Background(), createdID)
		s.ErrorIs(getErr, domain.ErrEntityNotFound, "Entity should not exist after rollback")
	})
}