package services

import (
	"errors"
	"fmt"
	"strings"
	"sync"
	"testing"

	"goyco/internal/database"
	"goyco/internal/repositories"

	"gorm.io/gorm"
)

type mockVoteRepo struct {
	votes               map[uint]*database.Vote
	byUserPost          map[string]*database.Vote
	byVoteHash          map[string]*database.Vote
	nextID              uint
	createErr           error
	getByUserAndPostErr error
	getByVoteHashErr    error
	getByPostIDErr      error
	updateErr           error
	deleteErr           error
	createCalls         int
	updateCalls         int
	deleteCalls         int
	mu                  sync.RWMutex
}

func newMockVoteRepo() *mockVoteRepo {
	return &mockVoteRepo{
		votes:      make(map[uint]*database.Vote),
		byUserPost: make(map[string]*database.Vote),
		byVoteHash: make(map[string]*database.Vote),
		nextID:     1,
	}
}

func (m *mockVoteRepo) Create(vote *database.Vote) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.createErr != nil {
		return m.createErr
	}

	var key string
	if vote.UserID != nil {
		key = m.key(*vote.UserID, vote.PostID)
	} else if vote.VoteHash != nil {
		key = *vote.VoteHash
	} else {
		return errors.New("vote must have either user_id or vote_hash")
	}

	if existingVote, exists := m.byUserPost[key]; exists {
		existingVote.Type = vote.Type
		existingVote.UpdatedAt = vote.UpdatedAt
		vote.ID = existingVote.ID
		return nil
	}

	vote.ID = m.nextID
	m.nextID++

	voteCopy := *vote
	m.votes[vote.ID] = &voteCopy
	m.byUserPost[key] = &voteCopy
	if vote.VoteHash != nil {
		m.byVoteHash[*vote.VoteHash] = &voteCopy
	}

	m.createCalls++
	return nil
}

func (m *mockVoteRepo) CreateOrUpdate(vote *database.Vote) error {
	return m.Create(vote)
}

func (m *mockVoteRepo) GetByID(id uint) (*database.Vote, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	if vote, ok := m.votes[id]; ok {
		voteCopy := *vote
		return &voteCopy, nil
	}
	return nil, gorm.ErrRecordNotFound
}

func (m *mockVoteRepo) GetByUserAndPost(userID, postID uint) (*database.Vote, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	if m.getByUserAndPostErr != nil {
		return nil, m.getByUserAndPostErr
	}

	key := m.key(userID, postID)
	if vote, ok := m.byUserPost[key]; ok {
		voteCopy := *vote
		return &voteCopy, nil
	}
	return nil, gorm.ErrRecordNotFound
}

func (m *mockVoteRepo) GetByVoteHash(voteHash string) (*database.Vote, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	if m.getByVoteHashErr != nil {
		return nil, m.getByVoteHashErr
	}

	if vote, ok := m.byVoteHash[voteHash]; ok {
		voteCopy := *vote
		return &voteCopy, nil
	}
	return nil, gorm.ErrRecordNotFound
}

func (m *mockVoteRepo) GetByPostID(postID uint) ([]database.Vote, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	if m.getByPostIDErr != nil {
		return nil, m.getByPostIDErr
	}

	var votes []database.Vote
	for _, vote := range m.votes {
		if vote.PostID == postID {
			votes = append(votes, *vote)
		}
	}
	return votes, nil
}

func (m *mockVoteRepo) GetByUserID(userID uint) ([]database.Vote, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var votes []database.Vote
	for _, vote := range m.votes {
		if vote.UserID != nil && *vote.UserID == userID {
			votes = append(votes, *vote)
		}
	}
	return votes, nil
}

func (m *mockVoteRepo) Update(vote *database.Vote) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.updateErr != nil {
		return m.updateErr
	}

	if _, ok := m.votes[vote.ID]; !ok {
		return gorm.ErrRecordNotFound
	}

	voteCopy := *vote
	m.votes[vote.ID] = &voteCopy

	var key string
	if vote.UserID != nil {
		key = m.key(*vote.UserID, vote.PostID)
		m.byUserPost[key] = &voteCopy
	}
	if vote.VoteHash != nil {
		m.byVoteHash[*vote.VoteHash] = &voteCopy
	}

	m.updateCalls++
	return nil
}

func (m *mockVoteRepo) Delete(id uint) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.deleteErr != nil {
		return m.deleteErr
	}

	vote, ok := m.votes[id]
	if !ok {
		return gorm.ErrRecordNotFound
	}

	delete(m.votes, id)

	var key string
	if vote.UserID != nil {
		key = m.key(*vote.UserID, vote.PostID)
		delete(m.byUserPost, key)
	}
	if vote.VoteHash != nil {
		delete(m.byVoteHash, *vote.VoteHash)
	}

	m.deleteCalls++
	return nil
}

func (m *mockVoteRepo) Count() (int64, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return int64(len(m.votes)), nil
}

func (m *mockVoteRepo) CountByPostID(postID uint) (int64, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	count := int64(0)
	for _, vote := range m.votes {
		if vote.PostID == postID {
			count++
		}
	}
	return count, nil
}

func (m *mockVoteRepo) CountByUserID(userID uint) (int64, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	count := int64(0)
	for _, vote := range m.votes {
		if vote.UserID != nil && *vote.UserID == userID {
			count++
		}
	}
	return count, nil
}

func (m *mockVoteRepo) GetVoteCountsByPostID(postID uint) (int, int, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	upVotes := 0
	downVotes := 0
	for _, vote := range m.votes {
		if vote.PostID == postID {
			switch vote.Type {
			case database.VoteUp:
				upVotes++
			case database.VoteDown:
				downVotes++
			}
		}
	}
	return upVotes, downVotes, nil
}

func (m *mockVoteRepo) WithTx(tx *gorm.DB) repositories.VoteRepository {
	return m
}

func (m *mockVoteRepo) key(userID, postID uint) string {
	return fmt.Sprintf("%d:%d", userID, postID)
}

type mockPostRepo struct {
	posts     map[uint]*database.Post
	nextID    uint
	getErr    error
	updateErr error
	mu        sync.RWMutex
}

func newMockPostRepo() *mockPostRepo {
	return &mockPostRepo{
		posts:  make(map[uint]*database.Post),
		nextID: 1,
	}
}

func (m *mockPostRepo) GetByID(id uint) (*database.Post, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	if m.getErr != nil {
		return nil, m.getErr
	}

	if post, ok := m.posts[id]; ok {
		postCopy := *post
		return &postCopy, nil
	}
	return nil, gorm.ErrRecordNotFound
}

func (m *mockPostRepo) Update(post *database.Post) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.updateErr != nil {
		return m.updateErr
	}

	if _, ok := m.posts[post.ID]; !ok {
		return gorm.ErrRecordNotFound
	}

	postCopy := *post
	m.posts[post.ID] = &postCopy
	return nil
}

func (m *mockPostRepo) Create(post *database.Post) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	post.ID = m.nextID
	m.nextID++

	postCopy := *post
	m.posts[post.ID] = &postCopy
	return nil
}

func (m *mockPostRepo) GetByURL(url string) (*database.Post, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	for _, post := range m.posts {
		if post.URL == url {
			postCopy := *post
			return &postCopy, nil
		}
	}
	return nil, gorm.ErrRecordNotFound
}

func (m *mockPostRepo) GetByAuthorID(authorID uint) ([]database.Post, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var posts []database.Post
	for _, post := range m.posts {
		if post.AuthorID != nil && *post.AuthorID == authorID {
			posts = append(posts, *post)
		}
	}
	return posts, nil
}

func (m *mockPostRepo) GetAll(limit, offset int) ([]database.Post, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var posts []database.Post
	count := 0
	for _, post := range m.posts {
		if count >= offset && count < offset+limit {
			posts = append(posts, *post)
		}
		count++
	}
	return posts, nil
}

func (m *mockPostRepo) Count() (int64, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return int64(len(m.posts)), nil
}

func (m *mockPostRepo) Delete(id uint) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if _, ok := m.posts[id]; !ok {
		return gorm.ErrRecordNotFound
	}

	delete(m.posts, id)
	return nil
}

func (m *mockPostRepo) GetByUserID(userID uint, limit, offset int) ([]database.Post, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var posts []database.Post
	count := 0
	for _, post := range m.posts {
		if post.AuthorID != nil && *post.AuthorID == userID {
			if count >= offset && count < offset+limit {
				posts = append(posts, *post)
			}
			count++
		}
	}
	return posts, nil
}

func (m *mockPostRepo) CountByUserID(userID uint) (int64, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	count := int64(0)
	for _, post := range m.posts {
		if post.AuthorID != nil && *post.AuthorID == userID {
			count++
		}
	}
	return count, nil
}

func (m *mockPostRepo) GetTopPosts(limit int) ([]database.Post, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var posts []database.Post
	count := 0
	for _, post := range m.posts {
		if count < limit {
			posts = append(posts, *post)
			count++
		}
	}
	return posts, nil
}

func (m *mockPostRepo) GetNewestPosts(limit int) ([]database.Post, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var posts []database.Post
	count := 0
	for _, post := range m.posts {
		if count < limit {
			posts = append(posts, *post)
			count++
		}
	}
	return posts, nil
}

func (m *mockPostRepo) Search(query string, limit, offset int) ([]database.Post, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var posts []database.Post
	count := 0
	for _, post := range m.posts {
		if strings.Contains(strings.ToLower(post.Title), strings.ToLower(query)) {
			if count >= offset && count < offset+limit {
				posts = append(posts, *post)
			}
			count++
		}
	}
	return posts, nil
}

func (m *mockPostRepo) GetPostsByDeletedUsers() ([]database.Post, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var posts []database.Post
	for _, post := range m.posts {
		if post.AuthorID == nil {
			posts = append(posts, *post)
		}
	}
	return posts, nil
}

func (m *mockPostRepo) HardDeletePostsByDeletedUsers() (int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	count := int64(0)
	for id, post := range m.posts {
		if post.AuthorID == nil {
			delete(m.posts, id)
			count++
		}
	}
	return count, nil
}

func (m *mockPostRepo) HardDeleteAll() (int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	count := int64(len(m.posts))
	m.posts = make(map[uint]*database.Post)
	return count, nil
}

func (m *mockPostRepo) WithTx(tx *gorm.DB) repositories.PostRepository {
	return m
}

func TestVoteService_CastVote_Authenticated(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	post := &database.Post{
		ID:        1,
		Title:     "Test Post",
		URL:       "https://example.com",
		AuthorID:  &[]uint{1}[0],
		UpVotes:   0,
		DownVotes: 0,
		Score:     0,
	}
	postRepo.posts[1] = post

	req := VoteRequest{
		UserID:    1,
		PostID:    1,
		Type:      database.VoteUp,
		IPAddress: "127.0.0.1",
		UserAgent: "test-agent",
	}

	result, err := service.CastVote(req)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if result == nil {
		t.Fatal("Expected result, got nil")
	}

	if result.Type != database.VoteUp {
		t.Errorf("Expected vote type 'up', got '%v'", result.Type)
	}

	if result.UpVotes != 1 {
		t.Errorf("Expected up votes to be 1, got %d", result.UpVotes)
	}

	if result.DownVotes != 0 {
		t.Errorf("Expected down votes to be 0, got %d", result.DownVotes)
	}

	if result.Score != 1 {
		t.Errorf("Expected score to be 1, got %d", result.Score)
	}

	if result.IsUnauthenticated {
		t.Error("Expected IsUnauthenticated to be false for authenticated vote")
	}
}

func TestVoteService_CastVote_Unauthenticated(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	post := &database.Post{
		ID:        1,
		Title:     "Test Post",
		URL:       "https://example.com",
		AuthorID:  &[]uint{1}[0],
		UpVotes:   0,
		DownVotes: 0,
		Score:     0,
	}
	postRepo.posts[1] = post

	req := VoteRequest{
		UserID:    0,
		PostID:    1,
		Type:      database.VoteUp,
		IPAddress: "127.0.0.1",
		UserAgent: "test-agent",
	}

	result, err := service.CastVote(req)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if result == nil {
		t.Fatal("Expected result, got nil")
	}

	if result.Type != database.VoteUp {
		t.Errorf("Expected vote type 'up', got '%v'", result.Type)
	}

	if result.UpVotes != 1 {
		t.Errorf("Expected up votes to be 1, got %d", result.UpVotes)
	}

	if result.DownVotes != 0 {
		t.Errorf("Expected down votes to be 0, got %d", result.DownVotes)
	}

	if result.Score != 1 {
		t.Errorf("Expected score to be 1, got %d", result.Score)
	}

	if !result.IsUnauthenticated {
		t.Error("Expected IsUnauthenticated to be true for unauthenticated vote")
	}
}

func TestVoteService_CastVote_UpdateExisting(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	post := &database.Post{
		ID:        1,
		Title:     "Test Post",
		URL:       "https://example.com",
		AuthorID:  &[]uint{1}[0],
		UpVotes:   0,
		DownVotes: 0,
		Score:     0,
	}
	postRepo.posts[1] = post

	req := VoteRequest{
		UserID:    1,
		PostID:    1,
		Type:      database.VoteUp,
		IPAddress: "127.0.0.1",
		UserAgent: "test-agent",
	}

	_, err := service.CastVote(req)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	req.Type = database.VoteDown
	result, err := service.CastVote(req)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if result.UpVotes != 0 {
		t.Errorf("Expected up votes to be 0, got %d", result.UpVotes)
	}

	if result.DownVotes != 1 {
		t.Errorf("Expected down votes to be 1, got %d", result.DownVotes)
	}

	if result.Score != -1 {
		t.Errorf("Expected score to be -1, got %d", result.Score)
	}
}

func TestVoteService_CastVote_RemoveVote(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	post := &database.Post{
		ID:        1,
		Title:     "Test Post",
		URL:       "https://example.com",
		AuthorID:  &[]uint{1}[0],
		UpVotes:   0,
		DownVotes: 0,
		Score:     0,
	}
	postRepo.posts[1] = post

	req := VoteRequest{
		UserID:    1,
		PostID:    1,
		Type:      database.VoteUp,
		IPAddress: "127.0.0.1",
		UserAgent: "test-agent",
	}

	_, err := service.CastVote(req)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	req.Type = database.VoteNone
	result, err := service.CastVote(req)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if result.UpVotes != 0 {
		t.Errorf("Expected up votes to be 0, got %d", result.UpVotes)
	}

	if result.DownVotes != 0 {
		t.Errorf("Expected down votes to be 0, got %d", result.DownVotes)
	}

	if result.Score != 0 {
		t.Errorf("Expected score to be 0, got %d", result.Score)
	}
}

func TestVoteService_GetUserVote_Authenticated(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	userID := uint(1)
	vote := &database.Vote{
		ID:     1,
		UserID: &userID,
		PostID: 1,
		Type:   database.VoteUp,
	}
	voteRepo.votes[1] = vote
	voteRepo.byUserPost["1:1"] = vote

	result, err := service.GetUserVote(1, 1, "127.0.0.1", "test-agent")
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if result == nil {
		t.Fatal("Expected vote, got nil")
	}

	if result.Type != database.VoteUp {
		t.Errorf("Expected vote type 'up', got '%v'", result.Type)
	}

	if result.UserID == nil || *result.UserID != 1 {
		t.Errorf("Expected user ID 1, got %v", result.UserID)
	}
}

func TestVoteService_GetUserVote_Unauthenticated(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	voteHash := service.GenerateVoteHash("127.0.0.1", "test-agent", 1)
	vote := &database.Vote{
		ID:       1,
		UserID:   nil,
		PostID:   1,
		Type:     database.VoteUp,
		VoteHash: &voteHash,
	}
	voteRepo.votes[1] = vote
	voteRepo.byVoteHash[voteHash] = vote

	result, err := service.GetUserVote(0, 1, "127.0.0.1", "test-agent")
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if result == nil {
		t.Fatal("Expected vote, got nil")
	}

	if result.Type != database.VoteUp {
		t.Errorf("Expected vote type 'up', got '%v'", result.Type)
	}

	if result.UserID != nil {
		t.Error("Expected UserID to be nil for unauthenticated vote")
	}

	if result.VoteHash == nil || *result.VoteHash != voteHash {
		t.Errorf("Expected vote hash '%s', got %v", voteHash, result.VoteHash)
	}
}

func TestVoteService_GetPostVotes(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	userID1 := uint(1)
	userID2 := uint(2)
	voteHash := "test-hash"

	vote1 := &database.Vote{
		ID:     1,
		UserID: &userID1,
		PostID: 1,
		Type:   database.VoteUp,
	}
	vote2 := &database.Vote{
		ID:     2,
		UserID: &userID2,
		PostID: 1,
		Type:   database.VoteDown,
	}
	vote3 := &database.Vote{
		ID:       3,
		UserID:   nil,
		PostID:   1,
		Type:     database.VoteUp,
		VoteHash: &voteHash,
	}

	voteRepo.votes[1] = vote1
	voteRepo.votes[2] = vote2
	voteRepo.votes[3] = vote3

	votes, err := service.GetPostVotes(1)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if len(votes) != 3 {
		t.Errorf("Expected 3 votes, got %d", len(votes))
	}

	hasAuthenticated := false
	hasUnauthenticated := false
	for _, vote := range votes {
		if vote.UserID != nil {
			hasAuthenticated = true
		}
		if vote.VoteHash != nil {
			hasUnauthenticated = true
		}
	}

	if !hasAuthenticated {
		t.Error("Expected to find authenticated votes")
	}

	if !hasUnauthenticated {
		t.Error("Expected to find unauthenticated votes")
	}
}

func TestVoteService_GetVoteStatistics(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	userID1 := uint(1)
	userID2 := uint(2)
	voteHash := "test-hash"

	vote1 := &database.Vote{
		ID:     1,
		UserID: &userID1,
		PostID: 1,
		Type:   database.VoteUp,
	}
	vote2 := &database.Vote{
		ID:     2,
		UserID: &userID2,
		PostID: 1,
		Type:   database.VoteDown,
	}
	vote3 := &database.Vote{
		ID:       3,
		UserID:   nil,
		PostID:   1,
		Type:     database.VoteUp,
		VoteHash: &voteHash,
	}

	voteRepo.votes[1] = vote1
	voteRepo.votes[2] = vote2
	voteRepo.votes[3] = vote3

	authenticatedCount, anonymousCount, err := service.GetVoteStatistics()
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if authenticatedCount != 3 {
		t.Errorf("Expected total count to be 3, got %d", authenticatedCount)
	}

	if anonymousCount != 0 {
		t.Errorf("Expected unauthenticated count to be 0, got %d", anonymousCount)
	}
}

func TestVoteService_Validation(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	req := VoteRequest{
		UserID:    1,
		PostID:    0,
		Type:      database.VoteUp,
		IPAddress: "127.0.0.1",
		UserAgent: "test-agent",
	}

	_, err := service.CastVote(req)
	if err == nil {
		t.Error("Expected error for missing post ID")
	}

	req.PostID = 1
	req.Type = "invalid"

	_, err = service.CastVote(req)
	if err == nil {
		t.Error("Expected error for invalid vote type")
	}
}

func TestVoteService_PostNotFound(t *testing.T) {
	voteRepo := newMockVoteRepo()
	postRepo := newMockPostRepo()
	service := NewVoteService(voteRepo, postRepo, nil)

	req := VoteRequest{
		UserID:    1,
		PostID:    999,
		Type:      database.VoteUp,
		IPAddress: "127.0.0.1",
		UserAgent: "test-agent",
	}

	_, err := service.CastVote(req)
	if err == nil {
		t.Error("Expected error for non-existent post")
	}

	if !strings.Contains(err.Error(), "post not found") {
		t.Errorf("Expected 'post not found' error, got %v", err)
	}
}
