package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/service"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"gorm.io/gorm"
)

// --- Async Test Mocks ---

type AsyncMockIdpProvider struct {
	mock.Mock
}

func (m *AsyncMockIdpProvider) Name() string { return "mock-idp" }
func (m *AsyncMockIdpProvider) GetMetadata() (*domain.IDPMetadata, error) {
	return &domain.IDPMetadata{}, nil
}

func (m *AsyncMockIdpProvider) UserExists(loginID string) (bool, error) {
	args := m.Called(loginID)
	return args.Bool(0), args.Error(1)
}

func (m *AsyncMockIdpProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) {
	args := m.Called(user, password)
	return args.String(0), args.Error(1)
}

func (m *AsyncMockIdpProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *AsyncMockIdpProvider) IssueSession(loginID string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *AsyncMockIdpProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) {
	return nil, nil
}

func (m *AsyncMockIdpProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *AsyncMockIdpProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
	return &domain.PasswordPolicy{MinLength: 12}, nil
}
func (m *AsyncMockIdpProvider) InitiatePasswordReset(loginID, redirectUrl string) error { return nil }
func (m *AsyncMockIdpProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *AsyncMockIdpProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
	return nil
}

type AsyncMockUserRepo struct {
	mock.Mock
	createCalled chan bool
}

func (m *AsyncMockUserRepo) Create(ctx context.Context, user *domain.User) error {
	// Simulate DB latency
	time.Sleep(50 * time.Millisecond)
	args := m.Called(ctx, user)
	if m.createCalled != nil {
		m.createCalled <- true
	}
	return args.Error(0)
}

func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error {
	args := m.Called(ctx, user)
	if m.createCalled != nil {
		m.createCalled <- true
	}
	return args.Error(0)
}

func (m *AsyncMockUserRepo) Delete(ctx context.Context, id string) error { return nil }
func (m *AsyncMockUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
	return nil, nil
}

func (m *AsyncMockUserRepo) FindByID(ctx context.Context, id string) (*domain.User, error) {
	return nil, nil
}

func (m *AsyncMockUserRepo) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) {
	return nil, nil
}

func (m *AsyncMockUserRepo) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
	return nil, nil
}

func (m *AsyncMockUserRepo) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
	return nil, 0, "", nil
}

func (m *AsyncMockUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
	return 0, nil
}

func (m *AsyncMockUserRepo) FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
	args := m.Called(ctx, tenantIDs)
	return args.Get(0).([]domain.User), args.Error(1)
}

func (m *AsyncMockUserRepo) CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error) {
	return nil, nil
}

func (m *AsyncMockUserRepo) FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error) {
	args := m.Called(ctx, codes)
	return args.Get(0).([]domain.User), args.Error(1)
}

func (m *AsyncMockUserRepo) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) {
	return nil, nil
}

func (m *AsyncMockUserRepo) DB() *gorm.DB {
	return nil
}

func (m *AsyncMockUserRepo) UpdateUserLoginIDs(ctx context.Context, userID string, loginIDs []domain.UserLoginID) error {
	return nil
}

func (m *AsyncMockUserRepo) GetUserLoginIDs(ctx context.Context, userID string) ([]domain.UserLoginID, error) {
	return nil, nil
}

func (m *AsyncMockUserRepo) IsLoginIDTaken(ctx context.Context, loginID string) (bool, error) {
	return false, nil
}

func (m *AsyncMockUserRepo) FindTenantIDByLoginID(ctx context.Context, loginID string) (string, error) {
	return "", nil
}

type AsyncMockRedisRepo struct {
	mock.Mock
}

func (m *AsyncMockRedisRepo) Set(key string, value string, expiration time.Duration) error {
	args := m.Called(key, value, expiration)
	return args.Error(0)
}

func (m *AsyncMockRedisRepo) Get(key string) (string, error) {
	args := m.Called(key)
	return args.String(0), args.Error(1)
}

func (m *AsyncMockRedisRepo) Delete(key string) error {
	args := m.Called(key)
	return args.Error(0)
}
func (m *AsyncMockRedisRepo) StoreVerificationCode(phone, code string) error   { return nil }
func (m *AsyncMockRedisRepo) GetVerificationCode(phone string) (string, error) { return "", nil }
func (m *AsyncMockRedisRepo) DeleteVerificationCode(phone string) error        { return nil }

type AsyncMockTenantService struct {
	mock.Mock
}

func (m *AsyncMockTenantService) RegisterTenant(ctx context.Context, name, slug, tenantType, description string, domains []string, parentID *string, creatorID string) (*domain.Tenant, error) {
	args := m.Called(ctx, name, slug, tenantType, description, domains, parentID, creatorID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.Tenant), args.Error(1)
}

func (m *AsyncMockTenantService) RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error) {
	return nil, nil
}

func (m *AsyncMockTenantService) GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) {
	args := m.Called(ctx, emailDomain)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.Tenant), args.Error(1)
}

func (m *AsyncMockTenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
	return nil, nil
}

func (m *AsyncMockTenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) {
	return nil, nil
}

func (m *AsyncMockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
	return nil, 0, nil
}

func (m *AsyncMockTenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
	return nil, nil
}

func (m *AsyncMockTenantService) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {
	return false, nil
}
func (m *AsyncMockTenantService) ApproveTenant(ctx context.Context, id string) error { return nil }
func (m *AsyncMockTenantService) ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
	return nil, nil
}
func (m *AsyncMockTenantService) SetKetoService(keto service.KetoService) {}
func (m *AsyncMockTenantService) AddTenantAdmin(ctx context.Context, tenantID, userID string) error {
	return nil
}

func (m *AsyncMockTenantService) RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error {
	return nil
}

func (m *AsyncMockTenantService) ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error) {
	return nil, nil
}

func (m *AsyncMockTenantService) DeleteTenantsBulk(ctx context.Context, ids []string) error {
	return nil
}

func (m *AsyncMockTenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
	args := m.Called(ctx, userID)
	if args.Get(0) != nil {
		return args.Get(0).([]domain.Tenant), args.Error(1)
	}
	return nil, args.Error(1)
}

type AsyncMockKetoService struct {
	mock.Mock
}

func (m *AsyncMockKetoService) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
	args := m.Called(ctx, namespace, object, relation, subject)
	return args.Error(0)
}

func (m *AsyncMockKetoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
	return nil
}

func (m *AsyncMockKetoService) CheckPermission(ctx context.Context, namespace, object, relation, subject string) (bool, error) {
	return false, nil
}

func (m *AsyncMockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
	return nil, nil
}

func (m *AsyncMockKetoService) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) {
	return nil, nil
}

// --- Tests ---

func TestSignup_AsyncDB_Isolation(t *testing.T) {
	mockIdp := new(AsyncMockIdpProvider)
	mockUserRepo := new(AsyncMockUserRepo)
	mockRedis := new(AsyncMockRedisRepo)
	mockTenant := new(AsyncMockTenantService)
	mockKeto := new(AsyncMockKetoService)

	h := &AuthHandler{
		IdpProvider:   mockIdp,
		UserRepo:      mockUserRepo,
		RedisService:  mockRedis,
		TenantService: mockTenant,
		KetoService:   mockKeto,
	}

	app := fiber.New()
	app.Post("/signup", h.Signup)

	t.Run("SoT_DB_Failure_Ignored_And_Async", func(t *testing.T) {
		email := "test@example.com"
		phone := "010-1234-5678"
		emailKey := "signup:email:" + email
		phoneKey := "signup:phone:" + "01012345678"

		// Redis Mocks
		mockRedis.On("Get", emailKey).Return(`{"verified": true, "expires_at": 9999999999}`, nil)
		mockRedis.On("Get", phoneKey).Return(`{"verified": true, "expires_at": 9999999999}`, nil)
		mockRedis.On("Delete", emailKey).Return(nil)
		mockRedis.On("Delete", phoneKey).Return(nil)

		// Tenant Mocks
		personalTenant := &domain.Tenant{ID: "personal-t1", Slug: "personal-test", Type: domain.TenantTypePersonal, Status: domain.TenantStatusActive}
		mockTenant.On("GetTenantByDomain", mock.Anything, "example.com").Return(nil, nil)
		mockTenant.On(
			"RegisterTenant",
			mock.Anything,
			"Personal - test@example.com",
			mock.MatchedBy(func(slug string) bool { return strings.HasPrefix(slug, "personal-") }),
			domain.TenantTypePersonal,
			"Automatically provisioned personal tenant",
			[]string(nil),
			(*string)(nil),
			"",
		).Return(personalTenant, nil)
		mockTenant.On("GetTenant", mock.Anything, "personal-t1").Return(personalTenant, nil)

		// Kratos Mocks (Success)
		mockIdp.On("CreateUser", mock.Anything, "Password123!").Return("new-user-uuid", nil)

		// UserRepo Mocks (Async & Failure)
		mockUserRepo.createCalled = make(chan bool, 1)
		mockUserRepo.On("Update", mock.Anything, mock.MatchedBy(func(u *domain.User) bool {
			return u.Email == email
		})).Return(errors.New("db connection error"))

		// Keto Mocks (Optional, since it's also async)
		// We won't block on this either

		body, _ := json.Marshal(domain.SignupRequest{
			Email:         email,
			Password:      "Password123!",
			Name:          "Test User",
			Phone:         phone,
			TermsAccepted: true,
		})
		req := httptest.NewRequest("POST", "/signup", bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")

		start := time.Now()
		resp, err := app.Test(req, 5000)
		elapsed := time.Since(start)

		if err != nil {
			t.Fatalf("Request failed: %v", err)
		}

		assert.Equal(t, 200, resp.StatusCode)

		// Ensure API responded faster than DB latency (50ms)
		assert.Less(t, int64(elapsed), int64(60*time.Millisecond), "API should return before DB timeout")

		// Wait for async execution
		select {
		case <-mockUserRepo.createCalled:
			// Pass
		case <-time.After(2 * time.Second):
			t.Fatal("UserRepo.Create was not called asynchronously")
		}

		mockRedis.AssertExpectations(t)
		mockIdp.AssertExpectations(t)
		mockUserRepo.AssertExpectations(t)
	})
}
