package manager

import (
	"context"
	"fmt"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/netbirdio/netbird/management/internals/modules/zones"
	"github.com/netbirdio/netbird/management/internals/modules/zones/records"
	"github.com/netbirdio/netbird/management/server/activity"
	"github.com/netbirdio/netbird/management/server/mock_server"
	"github.com/netbirdio/netbird/management/server/permissions"
	"github.com/netbirdio/netbird/management/server/permissions/modules"
	"github.com/netbirdio/netbird/management/server/permissions/operations"
	"github.com/netbirdio/netbird/management/server/store"
	"github.com/netbirdio/netbird/management/server/types"
	"github.com/netbirdio/netbird/shared/management/status"
)

const (
	testAccountID = "test-account-id"
	testUserID    = "test-user-id"
	testZoneID    = "test-zone-id"
	testGroupID   = "test-group-id"
	testDNSDomain = "netbird.selfhosted"
)

func setupTest(t *testing.T) (*managerImpl, store.Store, *mock_server.MockAccountManager, *permissions.MockManager, *gomock.Controller, func()) {
	t.Helper()

	ctx := context.Background()
	testStore, cleanup, err := store.NewTestStoreFromSQL(ctx, "", t.TempDir())
	require.NoError(t, err)

	err = testStore.SaveAccount(ctx, &types.Account{
		Id: testAccountID,
		Groups: map[string]*types.Group{
			testGroupID: {
				ID:   testGroupID,
				Name: "Test Group",
			},
		},
	})
	require.NoError(t, err)

	ctrl := gomock.NewController(t)
	mockAccountManager := &mock_server.MockAccountManager{}
	mockPermissionsManager := permissions.NewMockManager(ctrl)

	manager := &managerImpl{
		store:              testStore,
		accountManager:     mockAccountManager,
		permissionsManager: mockPermissionsManager,
		dnsDomain:          testDNSDomain,
	}

	return manager, testStore, mockAccountManager, mockPermissionsManager, ctrl, cleanup
}

func TestManagerImpl_GetAllZones(t *testing.T) {
	ctx := context.Background()

	t.Run("success", func(t *testing.T) {
		manager, testStore, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		zone1 := zones.NewZone(testAccountID, "Zone 1", "zone1.example.com", true, true, []string{testGroupID})
		err := testStore.CreateZone(ctx, zone1)
		require.NoError(t, err)

		zone2 := zones.NewZone(testAccountID, "Zone 2", "zone2.example.com", false, false, []string{testGroupID})
		err = testStore.CreateZone(ctx, zone2)
		require.NoError(t, err)

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
			Return(true, ctx, nil)

		result, err := manager.GetAllZones(ctx, testAccountID, testUserID)
		require.NoError(t, err)
		assert.Len(t, result, 2)
		assert.Equal(t, zone1.ID, result[0].ID)
		assert.Equal(t, zone2.ID, result[1].ID)
	})

	t.Run("permission denied", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
			Return(false, ctx, nil)

		result, err := manager.GetAllZones(ctx, testAccountID, testUserID)
		require.Error(t, err)
		assert.Nil(t, result)
		s, ok := status.FromError(err)
		assert.True(t, ok)
		assert.Equal(t, status.PermissionDenied, s.Type())
	})

	t.Run("permission validation error", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
			Return(false, ctx, status.Errorf(status.Internal, "permission check failed"))

		result, err := manager.GetAllZones(ctx, testAccountID, testUserID)
		require.Error(t, err)
		assert.Nil(t, result)
	})
}

func TestManagerImpl_GetZone(t *testing.T) {
	ctx := context.Background()

	t.Run("success", func(t *testing.T) {
		manager, testStore, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		zone := zones.NewZone(testAccountID, "Test Zone", "test.example.com", true, true, []string{testGroupID})
		err := testStore.CreateZone(ctx, zone)
		require.NoError(t, err)

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
			Return(true, ctx, nil)

		result, err := manager.GetZone(ctx, testAccountID, testUserID, zone.ID)
		require.NoError(t, err)
		assert.Equal(t, zone.ID, result.ID)
		assert.Equal(t, zone.Name, result.Name)
		assert.Equal(t, zone.Domain, result.Domain)
	})

	t.Run("permission denied", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
			Return(false, ctx, nil)

		result, err := manager.GetZone(ctx, testAccountID, testUserID, testZoneID)
		require.Error(t, err)
		assert.Nil(t, result)
		s, ok := status.FromError(err)
		assert.True(t, ok)
		assert.Equal(t, status.PermissionDenied, s.Type())
	})
}

func TestManagerImpl_CreateZone(t *testing.T) {
	ctx := context.Background()

	t.Run("success", func(t *testing.T) {
		manager, _, mockAccountManager, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		inputZone := &zones.Zone{
			Name:               "New Zone",
			Domain:             "new.example.com",
			Enabled:            true,
			EnableSearchDomain: true,
			DistributionGroups: []string{testGroupID},
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
			Return(true, ctx, nil)

		mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
			assert.Equal(t, testUserID, initiatorID)
			assert.Equal(t, testAccountID, accountID)
			assert.Equal(t, activity.DNSZoneCreated, activityID)
		}

		result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
		require.NoError(t, err)
		assert.NotNil(t, result)
		assert.NotEmpty(t, result.ID)
		assert.Equal(t, testAccountID, result.AccountID)
		assert.Equal(t, inputZone.Name, result.Name)
		assert.Equal(t, inputZone.Domain, result.Domain)
		assert.Equal(t, inputZone.Enabled, result.Enabled)
		assert.Equal(t, inputZone.EnableSearchDomain, result.EnableSearchDomain)
		assert.Equal(t, inputZone.DistributionGroups, result.DistributionGroups)
	})

	t.Run("permission denied", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		inputZone := &zones.Zone{
			Name:               "New Zone",
			Domain:             "new.example.com",
			DistributionGroups: []string{testGroupID},
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
			Return(false, ctx, nil)

		result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
		require.Error(t, err)
		assert.Nil(t, result)
		s, ok := status.FromError(err)
		assert.True(t, ok)
		assert.Equal(t, status.PermissionDenied, s.Type())
	})

	t.Run("invalid group", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		inputZone := &zones.Zone{
			Name:               "New Zone",
			Domain:             "new.example.com",
			DistributionGroups: []string{"invalid-group"},
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
			Return(true, ctx, nil)

		result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
		require.Error(t, err)
		assert.Nil(t, result)
	})

	t.Run("duplicate domain", func(t *testing.T) {
		manager, testStore, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		existingZone := zones.NewZone(testAccountID, "Existing Zone", "duplicate.example.com", true, false, []string{testGroupID})
		err := testStore.CreateZone(ctx, existingZone)
		require.NoError(t, err)

		inputZone := &zones.Zone{
			Name:               "New Zone",
			Domain:             "duplicate.example.com",
			Enabled:            true,
			EnableSearchDomain: false,
			DistributionGroups: []string{testGroupID},
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
			Return(true, ctx, nil)

		result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
		require.Error(t, err)
		assert.Nil(t, result)
		assert.Contains(t, err.Error(), "zone with domain duplicate.example.com already exists")
		s, ok := status.FromError(err)
		assert.True(t, ok)
		assert.Equal(t, status.AlreadyExists, s.Type())
	})

	t.Run("peer DNS domain conflict", func(t *testing.T) {
		manager, testStore, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		account, err := testStore.GetAccount(ctx, testAccountID)
		require.NoError(t, err)
		account.Settings.DNSDomain = "peers.example.com"
		err = testStore.SaveAccount(ctx, account)
		require.NoError(t, err)

		inputZone := &zones.Zone{
			Name:               "Test Zone",
			Domain:             "peers.example.com",
			Enabled:            true,
			EnableSearchDomain: false,
			DistributionGroups: []string{testGroupID},
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
			Return(true, ctx, nil)

		result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
		require.Error(t, err)
		assert.Nil(t, result)
		assert.Contains(t, err.Error(), "zone domain peers.example.com conflicts with peer DNS domain")
		s, ok := status.FromError(err)
		assert.True(t, ok)
		assert.Equal(t, status.InvalidArgument, s.Type())
	})

	t.Run("default DNS domain conflict", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		inputZone := &zones.Zone{
			Name:               "Test Zone",
			Domain:             testDNSDomain,
			Enabled:            true,
			EnableSearchDomain: false,
			DistributionGroups: []string{testGroupID},
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
			Return(true, ctx, nil)

		result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
		require.Error(t, err)
		assert.Nil(t, result)
		assert.Contains(t, err.Error(), fmt.Sprintf("zone domain %s conflicts with peer DNS domain", testDNSDomain))
		s, ok := status.FromError(err)
		assert.True(t, ok)
		assert.Equal(t, status.InvalidArgument, s.Type())
	})
}

func TestManagerImpl_UpdateZone(t *testing.T) {
	ctx := context.Background()

	t.Run("success", func(t *testing.T) {
		manager, testStore, mockAccountManager, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		existingZone := zones.NewZone(testAccountID, "Old Name", "example.com", false, false, []string{testGroupID})
		err := testStore.CreateZone(ctx, existingZone)
		require.NoError(t, err)

		updatedZone := &zones.Zone{
			ID:                 existingZone.ID,
			Name:               "Updated Name",
			Domain:             "example.com",
			Enabled:            true,
			EnableSearchDomain: true,
			DistributionGroups: []string{testGroupID},
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
			Return(true, ctx, nil)

		storeEventCalled := false
		mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
			storeEventCalled = true
			assert.Equal(t, testUserID, initiatorID)
			assert.Equal(t, existingZone.ID, targetID)
			assert.Equal(t, testAccountID, accountID)
			assert.Equal(t, activity.DNSZoneUpdated, activityID)
		}

		result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
		require.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, updatedZone.Name, result.Name)
		assert.Equal(t, updatedZone.Enabled, result.Enabled)
		assert.Equal(t, updatedZone.EnableSearchDomain, result.EnableSearchDomain)
		assert.True(t, storeEventCalled, "StoreEvent should have been called")
	})

	t.Run("domain change not allowed", func(t *testing.T) {
		manager, testStore, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		existingZone := zones.NewZone(testAccountID, "Test Zone", "example.com", true, true, []string{testGroupID})
		err := testStore.CreateZone(ctx, existingZone)
		require.NoError(t, err)

		updatedZone := &zones.Zone{
			ID:                 existingZone.ID,
			Name:               "Test Zone",
			Domain:             "different.com",
			Enabled:            true,
			EnableSearchDomain: true,
			DistributionGroups: []string{testGroupID},
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
			Return(true, ctx, nil)

		result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
		require.Error(t, err)
		assert.Nil(t, result)
		assert.Contains(t, err.Error(), "zone domain cannot be updated")
		s, ok := status.FromError(err)
		assert.True(t, ok)
		assert.Equal(t, status.InvalidArgument, s.Type())
	})

	t.Run("permission denied", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		updatedZone := &zones.Zone{
			ID:     testZoneID,
			Name:   "Updated Name",
			Domain: "example.com",
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
			Return(false, ctx, nil)

		result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
		require.Error(t, err)
		assert.Nil(t, result)
		s, ok := status.FromError(err)
		assert.True(t, ok)
		assert.Equal(t, status.PermissionDenied, s.Type())
	})

	t.Run("zone not found", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		updatedZone := &zones.Zone{
			ID:     "non-existent-zone",
			Name:   "Updated Name",
			Domain: "example.com",
		}

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
			Return(true, ctx, nil)

		result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
		require.Error(t, err)
		assert.Nil(t, result)
	})
}

func TestManagerImpl_DeleteZone(t *testing.T) {
	ctx := context.Background()

	t.Run("success with records", func(t *testing.T) {
		manager, testStore, mockAccountManager, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		zone := zones.NewZone(testAccountID, "Test Zone", "example.com", true, true, []string{testGroupID})
		err := testStore.CreateZone(ctx, zone)
		require.NoError(t, err)

		record1 := records.NewRecord(testAccountID, zone.ID, "api.example.com", records.RecordTypeA, "192.168.1.1", 300)
		err = testStore.CreateDNSRecord(ctx, record1)
		require.NoError(t, err)

		record2 := records.NewRecord(testAccountID, zone.ID, "www.example.com", records.RecordTypeA, "192.168.1.2", 300)
		err = testStore.CreateDNSRecord(ctx, record2)
		require.NoError(t, err)

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
			Return(true, ctx, nil)

		storeEventCallCount := 0
		mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
			storeEventCallCount++
			assert.Equal(t, testUserID, initiatorID)
			assert.Equal(t, testAccountID, accountID)
		}

		err = manager.DeleteZone(ctx, testAccountID, testUserID, zone.ID)
		require.NoError(t, err)
		assert.Equal(t, 3, storeEventCallCount)

		_, err = testStore.GetZoneByID(ctx, store.LockingStrengthNone, testAccountID, zone.ID)
		require.Error(t, err)

		zoneRecords, err := testStore.GetZoneDNSRecords(ctx, store.LockingStrengthNone, testAccountID, zone.ID)
		require.NoError(t, err)
		assert.Empty(t, zoneRecords)
	})

	t.Run("success without records", func(t *testing.T) {
		manager, testStore, mockAccountManager, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		zone := zones.NewZone(testAccountID, "Test Zone", "example.com", true, true, []string{testGroupID})
		err := testStore.CreateZone(ctx, zone)
		require.NoError(t, err)

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
			Return(true, ctx, nil)

		storeEventCalled := false
		mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
			storeEventCalled = true
			assert.Equal(t, testUserID, initiatorID)
			assert.Equal(t, zone.ID, targetID)
			assert.Equal(t, testAccountID, accountID)
			assert.Equal(t, activity.DNSZoneDeleted, activityID)
		}

		err = manager.DeleteZone(ctx, testAccountID, testUserID, zone.ID)
		require.NoError(t, err)
		assert.True(t, storeEventCalled, "StoreEvent should have been called")

		_, err = testStore.GetZoneByID(ctx, store.LockingStrengthNone, testAccountID, zone.ID)
		require.Error(t, err)
	})

	t.Run("permission denied", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
			Return(false, ctx, nil)

		err := manager.DeleteZone(ctx, testAccountID, testUserID, testZoneID)
		require.Error(t, err)
		s, ok := status.FromError(err)
		assert.True(t, ok)
		assert.Equal(t, status.PermissionDenied, s.Type())
	})

	t.Run("zone not found", func(t *testing.T) {
		manager, _, _, mockPermissionsManager, ctrl, cleanup := setupTest(t)
		defer cleanup()
		defer ctrl.Finish()

		mockPermissionsManager.EXPECT().
			ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
			Return(true, ctx, nil)

		err := manager.DeleteZone(ctx, testAccountID, testUserID, "non-existent-zone")
		require.Error(t, err)
	})
}
