// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge.  If not, see <https://www.gnu.org/licenses/>.

package useridentity

import (
	"context"
	"testing"

	"github.com/ProtonMail/go-proton-api"
	"github.com/ProtonMail/proton-bridge/v3/internal/events"
	mocks2 "github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
	"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
	"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity/mocks"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/require"
)

const TestUserID = "MyUserID"

func TestService_OnUserEvent(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, eventPublisher, _ := newTestService(t, mockCtrl)

	eventPublisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(events.UserChanged{UserID: TestUserID})).Times(1)

	require.NoError(t, service.HandleUserEvent(context.Background(), newTestUser()))
}

func TestService_OnUserSpaceChanged(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, eventPublisher, _ := newTestService(t, mockCtrl)

	eventPublisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(events.UsedSpaceChanged{UserID: TestUserID, UsedSpace: 1024})).Times(1)

	// Original value, no changes.
	require.NoError(t, service.HandleUsedSpaceEvent(context.Background(), 0))

	// New value, event should be published.
	require.NoError(t, service.HandleUsedSpaceEvent(context.Background(), 1024))
	require.Equal(t, uint64(1024), service.identity.User.UsedSpace)
}

func TestService_OnRefreshEvent(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, eventPublisher, provider := newTestService(t, mockCtrl)

	eventPublisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(events.UserRefreshed{UserID: TestUserID, CancelEventPool: false})).Times(1)

	newUser := newTestUserRefreshed()
	newAddresses := newTestAddressesRefreshed()

	{
		getUserCall := provider.EXPECT().GetUser(gomock.Any()).Times(1).Return(*newUser, nil)
		provider.EXPECT().GetAddresses(gomock.Any()).After(getUserCall).Times(1).Return(newAddresses, nil)
	}

	// Original value, no changes.
	require.NoError(t, service.HandleRefreshEvent(context.Background(), 0))

	require.Equal(t, *newUser, service.identity.User)
	require.Equal(t, newAddresses, service.identity.AddressesSorted)
}

func TestService_OnAddressCreated(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, eventPublisher, _ := newTestService(t, mockCtrl)

	newAddress := proton.Address{
		ID:     "NewAddrID",
		Email:  "new@bar.com",
		Status: proton.AddressStatusEnabled,
	}

	eventPublisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(events.UserAddressCreated{
		UserID:    TestUserID,
		AddressID: newAddress.ID,
		Email:     newAddress.Email,
	})).Times(1)

	err := service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     "",
				Action: proton.EventCreate,
			},
			Address: newAddress,
		},
	})
	require.NoError(t, err)

	require.Contains(t, service.identity.Addresses, newAddress.ID)
}

func TestService_OnAddressCreatedDisabledDoesNotProduceEvent(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, _, _ := newTestService(t, mockCtrl)

	newAddress := proton.Address{
		ID:     "Address1",
		Email:  "new@bar.com",
		Status: proton.AddressStatusEnabled,
	}

	err := service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     "",
				Action: proton.EventCreate,
			},
			Address: newAddress,
		},
	})
	require.NoError(t, err)

	require.Contains(t, service.identity.Addresses, newAddress.ID)
}

func TestService_OnAddressCreatedDuplicateDoesNotProduceEvent(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, _, _ := newTestService(t, mockCtrl)

	newAddress := proton.Address{
		ID:     "NewAddrID",
		Email:  "new@bar.com",
		Status: proton.AddressStatusDisabled,
	}

	err := service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     "",
				Action: proton.EventCreate,
			},
			Address: newAddress,
		},
	})
	require.NoError(t, err)

	require.Contains(t, service.identity.Addresses, newAddress.ID)
}

func TestService_OnAddressUpdated(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, eventPublisher, _ := newTestService(t, mockCtrl)

	newAddress := proton.Address{
		ID:     "Address1",
		Email:  "new@bar.com",
		Status: proton.AddressStatusEnabled,
	}

	eventPublisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(events.UserAddressUpdated{
		UserID:    TestUserID,
		AddressID: newAddress.ID,
		Email:     newAddress.Email,
	})).Times(1)

	err := service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     "",
				Action: proton.EventUpdate,
			},
			Address: newAddress,
		},
	})
	require.NoError(t, err)

	require.Equal(t, newAddress, service.identity.Addresses[newAddress.ID])
}

func TestService_OnAddressUpdatedDisableFollowedByEnable(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, eventPublisher, _ := newTestService(t, mockCtrl)

	newAddressDisabled := proton.Address{
		ID:     "Address1",
		Email:  "new@bar.com",
		Status: proton.AddressStatusDisabled,
	}
	newAddressEnabled := proton.Address{
		ID:     "Address1",
		Email:  "new@bar.com",
		Status: proton.AddressStatusEnabled,
	}

	{
		disabledCall := eventPublisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(events.UserAddressDisabled{
			UserID:    TestUserID,
			AddressID: newAddressDisabled.ID,
			Email:     newAddressDisabled.Email,
		})).Times(1)

		eventPublisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(events.UserAddressEnabled{
			UserID:    TestUserID,
			AddressID: newAddressEnabled.ID,
			Email:     newAddressEnabled.Email,
		})).Times(1).After(disabledCall)
	}

	err := service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     "",
				Action: proton.EventUpdate,
			},
			Address: newAddressDisabled,
		},
	})
	require.NoError(t, err)

	require.Equal(t, newAddressDisabled, service.identity.Addresses[newAddressEnabled.ID])

	err = service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     "",
				Action: proton.EventUpdate,
			},
			Address: newAddressEnabled,
		},
	})
	require.NoError(t, err)

	require.Equal(t, newAddressEnabled, service.identity.Addresses[newAddressEnabled.ID])
}

func TestService_OnAddressUpdateCreatedIfNotExists(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, eventPublisher, _ := newTestService(t, mockCtrl)

	newAddress := proton.Address{
		ID:     "NewAddrID",
		Email:  "new@bar.com",
		Status: proton.AddressStatusEnabled,
	}

	eventPublisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(events.UserAddressCreated{
		UserID:    TestUserID,
		AddressID: newAddress.ID,
		Email:     newAddress.Email,
	})).Times(1)

	err := service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     "",
				Action: proton.EventUpdate,
			},
			Address: newAddress,
		},
	})
	require.NoError(t, err)

	require.Contains(t, service.identity.Addresses, newAddress.ID)
}

func TestService_OnAddressDeleted(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, eventPublisher, _ := newTestService(t, mockCtrl)

	address := proton.Address{
		ID:     "Address1",
		Email:  "foo@bar.com",
		Status: proton.AddressStatusEnabled,
	}

	eventPublisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(events.UserAddressDeleted{
		UserID:    TestUserID,
		AddressID: address.ID,
		Email:     address.Email,
	})).Times(1)

	err := service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     address.ID,
				Action: proton.EventDelete,
			},
		},
	})
	require.NoError(t, err)

	require.NotContains(t, service.identity.Addresses, address.ID)
}

func TestService_OnAddressDeleteDisabledDoesNotProduceEvent(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, _, _ := newTestService(t, mockCtrl)

	address := proton.Address{
		ID:     "Address2",
		Email:  "foo2@bar.com",
		Status: proton.AddressStatusDisabled,
	}

	err := service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     address.ID,
				Action: proton.EventDelete,
			},
		},
	})
	require.NoError(t, err)

	require.NotContains(t, service.identity.Addresses, address.ID)
}

func TestService_OnAddressDeletedUnknownDoesNotProduceEvent(t *testing.T) {
	mockCtrl := gomock.NewController(t)

	service, _, _ := newTestService(t, mockCtrl)

	address := proton.Address{
		ID:     "UnknownID",
		Email:  "new@bar.com",
		Status: proton.AddressStatusEnabled,
	}

	err := service.HandleAddressEvents(context.Background(), []proton.AddressEvent{
		{
			EventItem: proton.EventItem{
				ID:     address.ID,
				Action: proton.EventDelete,
			},
			Address: address,
		},
	})
	require.NoError(t, err)
}

func newTestService(_ *testing.T, mockCtrl *gomock.Controller) (*Service, *mocks2.MockEventPublisher, *mocks.MockIdentityProvider) {
	subscribable := &userevents.NoOpSubscribable{}
	eventPublisher := mocks2.NewMockEventPublisher(mockCtrl)
	provider := mocks.NewMockIdentityProvider(mockCtrl)
	user := newTestUser()
	telemetry := mocks.NewMockTelemetry(mockCtrl)
	bridgePassProvider := NewFixedBridgePassProvider([]byte("hello"))

	service := NewService(subscribable, eventPublisher, NewState(*user, newTestAddresses(), provider), bridgePassProvider, telemetry)
	return service, eventPublisher, provider
}

func newTestUser() *proton.User {
	return &proton.User{
		ID:          TestUserID,
		Name:        "Foo",
		DisplayName: "Foo",
		Email:       "foo@bar",
		Keys:        nil,
		UsedSpace:   0,
		MaxSpace:    0,
		MaxUpload:   0,
		Credit:      0,
		Currency:    "",
	}
}

func newTestUserRefreshed() *proton.User {
	return &proton.User{
		ID:          TestUserID,
		Name:        "Alternate",
		DisplayName: "Universe",
		Email:       "foo2@bar",
		Keys:        nil,
		UsedSpace:   0,
		MaxSpace:    0,
		MaxUpload:   0,
		Credit:      0,
		Currency:    "USD",
	}
}

func newTestAddresses() []proton.Address {
	return []proton.Address{
		{
			ID:          "Address1",
			Email:       "foo@bar.com",
			Status:      proton.AddressStatusEnabled,
			Type:        0,
			Order:       0,
			DisplayName: "",
			Keys:        nil,
		},
		{
			ID:          "Address2",
			Email:       "foo2@bar.com",
			Status:      proton.AddressStatusDisabled,
			Type:        0,
			Order:       1,
			DisplayName: "",
			Keys:        nil,
		},
	}
}

func newTestAddressesRefreshed() []proton.Address {
	return []proton.Address{
		{
			ID:          "Address1",
			Email:       "foo@bar.com",
			Status:      proton.AddressStatusEnabled,
			Type:        0,
			Order:       2,
			DisplayName: "FOo barish",
			Keys:        nil,
		},
		{
			ID:          "Address2",
			Email:       "foo2@bar.com",
			Status:      proton.AddressStatusDisabled,
			Type:        0,
			Order:       4,
			DisplayName: "New display name",
			Keys:        nil,
		},
	}
}
