// Copyright (c) 2025 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 bridge_test

import (
	"testing"
	"time"

	"github.com/ProtonMail/go-proton-api"
	"github.com/ProtonMail/go-proton-api/server"
	"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
	"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
	"github.com/stretchr/testify/require"
	"golang.org/x/net/context"
)

func TestBridge_Observability(t *testing.T) {
	testMetric := proton.ObservabilityMetric{
		Name:      "test1",
		Version:   1,
		Timestamp: time.Now().Unix(),
		Data:      nil,
	}

	withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
		throttlePeriod := time.Millisecond * 500
		observability.ModifyThrottlePeriod(throttlePeriod)

		withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
			require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))

			bridge.PushObservabilityMetric(testMetric)
			time.Sleep(time.Millisecond * 50) // Wait for the metric to be sent
			require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))

			for i := 0; i < 10; i++ {
				time.Sleep(time.Millisecond * 5) // Minor delay between each so our tests aren't flaky
				bridge.PushObservabilityMetric(testMetric)
			}
			// We should still have only 1 metric sent as the throttleDuration has not passed
			require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))

			// Wait for throttle duration to pass; we should have our remaining metrics posted
			time.Sleep(throttlePeriod)
			require.Equal(t, 11, len(s.GetObservabilityStatistics().Metrics))

			// Wait for the throttle duration to reset; i.e. so we have enough time to send a request immediately
			time.Sleep(throttlePeriod)
			for i := 0; i < 10; i++ {
				time.Sleep(time.Millisecond * 5)
				bridge.PushObservabilityMetric(testMetric)
			}
			// We should only have one additional metric sent immediately
			require.Equal(t, 12, len(s.GetObservabilityStatistics().Metrics))

			// Wait for the others to be sent
			time.Sleep(throttlePeriod)
			require.Equal(t, 21, len(s.GetObservabilityStatistics().Metrics))

			// Spam the endpoint a bit
			for i := 0; i < 300; i++ {
				if i < 200 {
					time.Sleep(time.Millisecond * 10)
				}
				bridge.PushObservabilityMetric(testMetric)
			}

			// Ensure we've sent all metrics
			time.Sleep(throttlePeriod)

			observabilityStats := s.GetObservabilityStatistics()
			require.Equal(t, 321, len(observabilityStats.Metrics))

			// Verify that each request had a throttleDuration time difference between each request
			for i := 0; i < len(observabilityStats.RequestTime)-1; i++ {
				tOne := observabilityStats.RequestTime[i]
				tTwo := observabilityStats.RequestTime[i+1]
				require.True(t, tTwo.Sub(tOne).Abs() > throttlePeriod)
			}
		})
	})
}

func TestBridge_Observability_Heartbeat(t *testing.T) {
	withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
		throttlePeriod := time.Millisecond * 300
		observability.ModifyThrottlePeriod(throttlePeriod)

		withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
			require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
			bridge.ModifyObservabilityHeartbeatInterval(throttlePeriod)

			require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
			time.Sleep(time.Millisecond * 150)
			require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
			time.Sleep(time.Millisecond * 200)
			require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
			time.Sleep(time.Millisecond * 350)
			require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
			time.Sleep(time.Millisecond * 350)
			require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
		})
	})
}

func TestBridge_Observability_UserMetric(t *testing.T) {
	testMetric := proton.ObservabilityMetric{
		Name:      "test1",
		Version:   1,
		Timestamp: time.Now().Unix(),
		Data:      nil,
	}

	withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
		userMetricPeriod := time.Millisecond * 200
		heartbeatPeriod := time.Second * 10
		throttlePeriod := time.Millisecond * 100
		observability.ModifyUserMetricInterval(userMetricPeriod)
		observability.ModifyThrottlePeriod(throttlePeriod)

		withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
			require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
			bridge.ModifyObservabilityHeartbeatInterval(heartbeatPeriod)

			time.Sleep(throttlePeriod)
			require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))

			bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
			time.Sleep(throttlePeriod)
			// We're expecting two observability metrics to be sent, the actual metric + the user metric.
			require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))

			bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
			time.Sleep(throttlePeriod)
			// We're expecting only a single metric to be sent, since the user metric update has been sent already within the predefined period.
			require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))

			bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
			time.Sleep(throttlePeriod)
			// Two metric updates should be sent again.
			require.Equal(t, 5, len(s.GetObservabilityStatistics().Metrics))

			bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
			time.Sleep(throttlePeriod)
			// Only a single one should be sent.
			require.Equal(t, 6, len(s.GetObservabilityStatistics().Metrics))
		})
	})
}
