// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package db_test

import (
	"context"
	"fmt"
	"testing"
	"time"

	db "github.com/gitpod-io/gitpod/components/gitpod-db/go"

	"github.com/gitpod-io/gitpod/components/gitpod-db/go/dbtest"
	"github.com/google/uuid"
	"github.com/stretchr/testify/require"
)

func TestFindUsageInRange(t *testing.T) {
	conn := dbtest.ConnectForTests(t)

	start := time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC)
	end := time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC)

	attributionID := db.NewTeamAttributionID(uuid.New().String())

	entryBefore := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(-1 * 23 * time.Hour)),
		Draft:         true,
	})

	entryInside := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Minute)),
	})

	entryAfter := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(end.Add(2 * time.Hour)),
	})

	usageEntries := []db.Usage{entryBefore, entryInside, entryAfter}
	dbtest.CreateUsageRecords(t, conn, usageEntries...)
	listResult, err := db.FindUsage(context.Background(), conn, &db.FindUsageParams{
		AttributionId: attributionID,
		From:          start,
		To:            end,
	})
	require.NoError(t, err)

	require.Equal(t, 1, len(listResult))
	require.Equal(t, []db.Usage{entryInside}, listResult)
}

func TestGetUsageSummary(t *testing.T) {
	conn := dbtest.ConnectForTests(t)

	start := time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC)
	end := time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC)

	attributionID := db.NewTeamAttributionID(uuid.New().String())

	draftBefore := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(-1 * 23 * time.Hour)),
		CreditCents:   100,
		Draft:         true,
	})
	nondraftBefore := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(-1 * 23 * time.Hour)),
		CreditCents:   200,
		Draft:         false,
	})

	draftInside := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Hour)),
		CreditCents:   300,
		Draft:         true,
	})
	nonDraftInside := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Hour)),
		CreditCents:   400,
		Draft:         false,
	})

	nonDraftAfter := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(end.Add(2 * time.Hour)),
		CreditCents:   1000,
	})

	invoice := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		Kind:          db.InvoiceUsageKind,
		EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Hour)),
		CreditCents:   -400,
		Draft:         false,
	})

	dbtest.CreateUsageRecords(t, conn, draftBefore, nondraftBefore, draftInside, nonDraftInside, nonDraftAfter, invoice)

	tests := []struct {
		start, end    time.Time
		excludeDrafts bool
		// expectations
		creditCents     db.CreditCents
		numberOfRecords int
	}{
		{start, end, false, 700, 2},
		{start, end, true, 400, 1},
		{end, end, false, 0, 0},
		{end, end, true, 0, 0},
		{start, start, false, 0, 0},
		{start.Add(-500 * 24 * time.Hour), end, false, 1000, 4},
		{start.Add(-500 * 24 * time.Hour), end.Add(500 * 24 * time.Hour), false, 2000, 5},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("Running test no %d", i+1), func(t *testing.T) {
			usageSummary, err := db.GetUsageSummary(context.Background(), conn, db.GetUsageSummaryParams{
				AttributionId: attributionID,
				From:          test.start,
				To:            test.end,
				ExcludeDrafts: test.excludeDrafts,
			})
			require.NoError(t, err)

			require.EqualValues(t, test.creditCents, usageSummary.CreditCentsUsed)
			require.EqualValues(t, test.numberOfRecords, usageSummary.NumberOfRecords)
		})
	}
}

func TestInsertUsageRecords(t *testing.T) {
	conn := dbtest.ConnectForTests(t)

	attributionID := db.NewTeamAttributionID(uuid.New().String())
	start := time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC)

	usage := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Hour)),
		Draft:         true,
	})

	dbtest.CreateUsageRecords(t, conn, usage)
	updatedDesc := "Updated Description"
	usage.Description = updatedDesc

	require.NoError(t, db.InsertUsage(context.Background(), conn, usage))

	drafts, err := db.FindAllDraftUsage(context.Background(), conn)
	require.NoError(t, err)
	cleaned := filter(drafts, attributionID)
	require.Equal(t, 1, len(cleaned))
	require.NotEqual(t, updatedDesc, cleaned[0].Description)
}

func filter(drafts []db.Usage, attributionID db.AttributionID) []db.Usage {
	var cleaned []db.Usage
	for _, draft := range drafts {
		if draft.AttributionID == attributionID {
			cleaned = append(cleaned, draft)
		}
	}
	return cleaned
}

func TestUpdateUsageRecords(t *testing.T) {
	conn := dbtest.ConnectForTests(t)

	attributionID := db.NewTeamAttributionID(uuid.New().String())
	start := time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC)

	usage := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Hour)),
		Draft:         true,
	})

	dbtest.CreateUsageRecords(t, conn, usage)
	updatedDesc := "Updated Description"
	usage.Description = updatedDesc

	require.NoError(t, db.UpdateUsage(context.Background(), conn, usage))

	drafts, err := db.FindAllDraftUsage(context.Background(), conn)
	require.NoError(t, err)
	cleaned := filter(drafts, attributionID)
	require.Equal(t, 1, len(cleaned))
	require.Equal(t, updatedDesc, cleaned[0].Description)
}

func TestFindAllDraftUsage(t *testing.T) {
	conn := dbtest.ConnectForTests(t)

	attributionID := db.NewTeamAttributionID(uuid.New().String())
	start := time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC)

	usage1 := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Hour)),
		Draft:         true,
	})
	usage2 := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Hour)),
		Draft:         true,
	})
	usage3 := dbtest.NewUsage(t, db.Usage{
		AttributionID: attributionID,
		EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Hour)),
		Draft:         false,
	})

	dbtest.CreateUsageRecords(t, conn, usage1, usage2, usage3)
	drafts, err := db.FindAllDraftUsage(context.Background(), conn)
	require.NoError(t, err)
	cleaned := filter(drafts, attributionID)
	require.Equal(t, 2, len(cleaned))
	for _, usage := range cleaned {
		require.True(t, usage.Draft)
	}

	// let's finalize one record
	usage2.Draft = false
	require.NoError(t, db.UpdateUsage(context.Background(), conn, usage2))

	drafts, err = db.FindAllDraftUsage(context.Background(), conn)
	require.NoError(t, err)
	cleaned = filter(drafts, attributionID)
	require.Equal(t, 1, len(cleaned))
	for _, usage := range cleaned {
		require.True(t, usage.Draft)
	}
}

func TestCreditCents(t *testing.T) {
	for _, s := range []struct {
		value           float64
		expected        db.CreditCents
		expectedAsFloat float64
	}{
		{
			value:           0,
			expected:        0,
			expectedAsFloat: 0,
		},
		{
			value:           0.1,
			expected:        10,
			expectedAsFloat: 0.1,
		},
		{
			value:           1.1111,
			expected:        111,
			expectedAsFloat: 1.11,
		},
		{
			value:           1.4999,
			expected:        150,
			expectedAsFloat: 1.50,
		},
		{
			value:           1.500,
			expected:        150,
			expectedAsFloat: 1.50,
		},
		{
			value:           1.501,
			expected:        150,
			expectedAsFloat: 1.50,
		},
		{
			value:           1.50999,
			expected:        151,
			expectedAsFloat: 1.51,
		},
		{
			value:           1.9999,
			expected:        200,
			expectedAsFloat: 2.00,
		},
		{
			value:           -1.9999,
			expected:        -200,
			expectedAsFloat: -2.00,
		},
	} {
		cc := db.NewCreditCents(s.value)
		require.Equal(t, s.expected, cc)
		require.Equal(t, s.expectedAsFloat, cc.ToCredits())
	}
}

func TestListBalance(t *testing.T) {
	teamAttributionID := db.NewTeamAttributionID(uuid.New().String())
	userAttributionID := db.NewUserAttributionID(uuid.New().String())

	conn := dbtest.ConnectForTests(t)
	dbtest.CreateUsageRecords(t, conn,
		dbtest.NewUsage(t, db.Usage{
			AttributionID: teamAttributionID,
			CreditCents:   100,
		}),
		dbtest.NewUsage(t, db.Usage{
			AttributionID: teamAttributionID,
			CreditCents:   900,
		}),
		dbtest.NewUsage(t, db.Usage{
			AttributionID: userAttributionID,
			CreditCents:   450,
		}),
		dbtest.NewUsage(t, db.Usage{
			AttributionID: userAttributionID,
			CreditCents:   -500,
			Kind:          db.InvoiceUsageKind,
		}),
	)

	balances, err := db.ListBalance(context.Background(), conn)
	require.NoError(t, err)
	require.Contains(t, balances, db.Balance{
		AttributionID: teamAttributionID,
		CreditCents:   1000,
	})
	require.Contains(t, balances, db.Balance{
		AttributionID: userAttributionID,
		CreditCents:   -50,
	})
}

func TestGetBalance(t *testing.T) {
	teamAttributionID := db.NewTeamAttributionID(uuid.New().String())
	userAttributionID := db.NewUserAttributionID(uuid.New().String())
	noUsageAttributionID := db.NewUserAttributionID(uuid.New().String())

	conn := dbtest.ConnectForTests(t)
	dbtest.CreateUsageRecords(t, conn,
		dbtest.NewUsage(t, db.Usage{
			AttributionID: teamAttributionID,
			CreditCents:   100,
		}),
		dbtest.NewUsage(t, db.Usage{
			AttributionID: teamAttributionID,
			CreditCents:   900,
		}),
		dbtest.NewUsage(t, db.Usage{
			AttributionID: userAttributionID,
			CreditCents:   450,
		}),
		dbtest.NewUsage(t, db.Usage{
			AttributionID: userAttributionID,
			CreditCents:   -500,
			Kind:          db.InvoiceUsageKind,
		}),
	)

	teamBalance, err := db.GetBalance(context.Background(), conn, teamAttributionID)
	require.NoError(t, err)
	require.EqualValues(t, 1000, int(teamBalance))

	userBalance, err := db.GetBalance(context.Background(), conn, userAttributionID)
	require.NoError(t, err)
	require.EqualValues(t, -50, int(userBalance))

	noUsageBalance, err := db.GetBalance(context.Background(), conn, noUsageAttributionID)
	require.NoError(t, err)
	require.EqualValues(t, 0, int(noUsageBalance))
}
