package face

import (
	"context"
	"testing"
	"time"
)

// MockPhotoRepository is a mock implementation of PhotoRepository for testing
type MockPhotoRepository struct {
	photos    map[string]*PhotoInfo
	embeddings []*FaceEmbeddingInfo
}

func NewMockPhotoRepository() *MockPhotoRepository {
	return &MockPhotoRepository{
		photos: make(map[string]*PhotoInfo),
	}
}

func (m *MockPhotoRepository) GetPhoto(ctx context.Context, id string) (*PhotoInfo, error) {
	if photo, ok := m.photos[id]; ok {
		return photo, nil
	}
	return &PhotoInfo{
		ID:      id,
		SpaceID: "test-space",
		URL:     "https://example.com/photo.jpg",
	}, nil
}

func (m *MockPhotoRepository) CreateFaceEmbedding(ctx context.Context, embedding *FaceEmbeddingInfo) (*FaceEmbeddingInfo, error) {
	m.embeddings = append(m.embeddings, embedding)
	return embedding, nil
}

func (m *MockPhotoRepository) AddPhoto(photo *PhotoInfo) {
	m.photos[photo.ID] = photo
}

// Test helper functions

func setupTestService() (*Service, *MockClient, *MockPhotoRepository, *MemoryCache) {
	mockClient := NewMockClient()
	mockRepo := NewMockPhotoRepository()
	cache := NewMemoryCache()
	config := DefaultConfig()

	service := NewService(mockClient, mockRepo, cache, config)
	return service, mockClient, mockRepo, cache
}

// Tests

func TestService_IndexPhoto_Success(t *testing.T) {
	service, mockClient, mockRepo, _ := setupTestService()
	ctx := context.Background()

	// Test data
	photoID := "test-photo-1"
	imageBytes := []byte("test-image-data")

	// Add photo to mock repo
	mockRepo.AddPhoto(&PhotoInfo{
		ID:      photoID,
		SpaceID: "test-space-1",
	})

	// Execute
	err := service.IndexPhoto(ctx, photoID, imageBytes)

	// Assert
	if err != nil {
		t.Errorf("Expected no error, got: %v", err)
	}
	_ = mockClient // Client was called internally
}

func TestService_IndexPhoto_AlreadyIndexed(t *testing.T) {
	service, _, _, cache := setupTestService()
	ctx := context.Background()

	// Test data
	photoID := "test-photo-1"
	imageBytes := []byte("test-image-data")

	// Pre-populate cache
	cache.Set(ctx, "indexed:"+photoID, "true", 24*time.Hour)

	// Execute
	err := service.IndexPhoto(ctx, photoID, imageBytes)

	// Assert
	if err != nil {
		t.Errorf("Expected no error, got: %v", err)
	}
}

func TestService_SearchFaces_Success(t *testing.T) {
	service, _, mockRepo, _ := setupTestService()
	ctx := context.Background()

	// Test data
	spaceID := "test-space-1"
	imageBytes := []byte("test-image-data")
	limit := 10

	// Add photo to mock repo
	mockRepo.AddPhoto(&PhotoInfo{
		ID:      "photo-1",
		SpaceID: spaceID,
		URL:     "https://example.com/photo-1.jpg",
	})

	// Execute
	result, err := service.SearchFaces(ctx, spaceID, imageBytes, limit)

	// Assert
	if err != nil {
		t.Errorf("Expected no error, got: %v", err)
	}
	if result == nil {
		t.Fatal("Expected result, got nil")
	}
	if len(result.Matches) == 0 {
		t.Error("Expected at least one match")
	}
	if result.CacheHit {
		t.Error("Expected cache hit to be false")
	}
}

func TestService_SearchFaces_CacheHit(t *testing.T) {
	service, _, _, cache := setupTestService()
	ctx := context.Background()

	// Test data
	spaceID := "test-space-1"
	imageBytes := []byte("test-image-data")
	limit := 10

	// Pre-populate cache with a result
	cachedResult := &FaceSearchResult{
		Matches: []FaceMatch{
			{
				FaceID:     "cached-face-1",
				Similarity: 95.0,
			},
		},
		PhotoCount: 1,
	}
	cache.SetJSON(ctx, "search:"+spaceID+":"+computeHash(imageBytes), cachedResult, 5*time.Minute)

	// Execute
	result, err := service.SearchFaces(ctx, spaceID, imageBytes, limit)

	// Assert
	if err != nil {
		t.Errorf("Expected no error, got: %v", err)
	}
	if result == nil {
		t.Fatal("Expected result, got nil")
	}
	if !result.CacheHit {
		t.Error("Expected cache hit to be true")
	}
	if result.PhotoCount != 1 {
		t.Errorf("Expected 1 photo, got %d", result.PhotoCount)
	}
}

func TestService_CreateCollection(t *testing.T) {
	service, _, _, _ := setupTestService()
	ctx := context.Background()

	// Execute
	err := service.CreateCollection(ctx, "test-collection")

	// Assert
	if err != nil {
		t.Errorf("Expected no error, got: %v", err)
	}
}

func TestService_DeleteCollection(t *testing.T) {
	service, _, _, _ := setupTestService()
	ctx := context.Background()

	// Execute
	err := service.DeleteCollection(ctx, "test-collection")

	// Assert
	if err != nil {
		t.Errorf("Expected no error, got: %v", err)
	}
}

func TestConfig_Default(t *testing.T) {
	config := DefaultConfig()

	if config.Region != "us-east-1" {
		t.Errorf("Expected region us-east-1, got %s", config.Region)
	}
	if config.CollectionID != "hatch-faces" {
		t.Errorf("Expected collection ID hatch-faces, got %s", config.CollectionID)
	}
	if config.MaxFaces != 10 {
		t.Errorf("Expected max faces 10, got %d", config.MaxFaces)
	}
	if config.Threshold != 80.0 {
		t.Errorf("Expected threshold 80.0, got %f", config.Threshold)
	}
	if config.SearchCacheTTL != 5*time.Minute {
		t.Errorf("Expected search cache TTL 5m, got %v", config.SearchCacheTTL)
	}
}

func TestConfig_Validate(t *testing.T) {
	// Valid config
	config := DefaultConfig()
	if err := config.Validate(); err != nil {
		t.Errorf("Expected valid config, got error: %v", err)
	}

	// Invalid: missing region
	invalidConfig := DefaultConfig()
	invalidConfig.Region = ""
	if err := invalidConfig.Validate(); err == nil {
		t.Error("Expected error for missing region")
	}

	// Invalid: missing collection ID
	invalidConfig2 := DefaultConfig()
	invalidConfig2.CollectionID = ""
	if err := invalidConfig2.Validate(); err == nil {
		t.Error("Expected error for missing collection ID")
	}
}

func TestCacheKeys(t *testing.T) {
	keys := CacheKeys{}

	if keys.Search("space1", "hash1") != "search:space1:hash1" {
		t.Error("Search key mismatch")
	}
	if keys.Photo("photo1") != "photo:photo1" {
		t.Error("Photo key mismatch")
	}
	if keys.Face("face1") != "face:face1" {
		t.Error("Face key mismatch")
	}
	if keys.Indexed("photo1") != "indexed:photo1" {
		t.Error("Indexed key mismatch")
	}
}

func TestValidateImage(t *testing.T) {
	service, _, _, _ := setupTestService()

	// Test with valid JPEG header (need at least 100 bytes)
	validJPEG := make([]byte, 200)
	validJPEG[0] = 0xFF
	validJPEG[1] = 0xD8
	validJPEG[2] = 0xFF
	validJPEG[3] = 0xE0
	if err := service.ValidateImage(validJPEG); err != nil {
		t.Errorf("Expected no error for valid JPEG, got: %v", err)
	}

	// Test with valid PNG header (need at least 100 bytes)
	validPNG := make([]byte, 200)
	validPNG[0] = 0x89
	validPNG[1] = 0x50
	validPNG[2] = 0x4E
	validPNG[3] = 0x47
	if err := service.ValidateImage(validPNG); err != nil {
		t.Errorf("Expected no error for valid PNG, got: %v", err)
	}

	// Test with too large image
	largeImage := make([]byte, 11*1024*1024) // 11MB
	if err := service.ValidateImage(largeImage); err != ErrImageTooLarge {
		t.Errorf("Expected ErrImageTooLarge, got: %v", err)
	}

	// Test with too small image
	smallImage := make([]byte, 10)
	if err := service.ValidateImage(smallImage); err != ErrImageTooSmall {
		t.Errorf("Expected ErrImageTooSmall, got: %v", err)
	}

	// Test with invalid format (large enough but wrong header)
	invalidImage := make([]byte, 200)
	if err := service.ValidateImage(invalidImage); err != ErrInvalidImageFormat {
		t.Errorf("Expected ErrInvalidImageFormat, got: %v", err)
	}
}

func TestComputeHash(t *testing.T) {
	data1 := []byte("test data for hashing")
	data2 := []byte("different data for hashing")

	hash1 := computeHash(data1)
	hash2 := computeHash(data2)

	if hash1 == hash2 {
		t.Error("Expected different hashes for different data")
	}

	// Same data should produce same hash
	hash1Again := computeHash(data1)
	if hash1 != hash1Again {
		t.Error("Expected same hash for same data")
	}
}
