// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package quota_test

import (
	"testing"

	quota_model "code.gitea.io/gitea/models/quota"

	"github.com/stretchr/testify/assert"
)

func makeFullyUsed() quota_model.Used {
	return quota_model.Used{
		Size: quota_model.UsedSize{
			Repos: quota_model.UsedSizeRepos{
				Public:  1024,
				Private: 1024,
			},
			Git: quota_model.UsedSizeGit{
				LFS: 1024,
			},
			Assets: quota_model.UsedSizeAssets{
				Attachments: quota_model.UsedSizeAssetsAttachments{
					Issues:   1024,
					Releases: 1024,
				},
				Artifacts: 1024,
				Packages: quota_model.UsedSizeAssetsPackages{
					All: 1024,
				},
			},
		},
	}
}

func makePartiallyUsed() quota_model.Used {
	return quota_model.Used{
		Size: quota_model.UsedSize{
			Repos: quota_model.UsedSizeRepos{
				Public: 1024,
			},
			Assets: quota_model.UsedSizeAssets{
				Attachments: quota_model.UsedSizeAssetsAttachments{
					Releases: 1024,
				},
			},
		},
	}
}

func setUsed(used quota_model.Used, subject quota_model.LimitSubject, value int64) *quota_model.Used {
	switch subject {
	case quota_model.LimitSubjectSizeReposPublic:
		used.Size.Repos.Public = value
		return &used
	case quota_model.LimitSubjectSizeReposPrivate:
		used.Size.Repos.Private = value
		return &used
	case quota_model.LimitSubjectSizeGitLFS:
		used.Size.Git.LFS = value
		return &used
	case quota_model.LimitSubjectSizeAssetsAttachmentsIssues:
		used.Size.Assets.Attachments.Issues = value
		return &used
	case quota_model.LimitSubjectSizeAssetsAttachmentsReleases:
		used.Size.Assets.Attachments.Releases = value
		return &used
	case quota_model.LimitSubjectSizeAssetsArtifacts:
		used.Size.Assets.Artifacts = value
		return &used
	case quota_model.LimitSubjectSizeAssetsPackagesAll:
		used.Size.Assets.Packages.All = value
		return &used
	case quota_model.LimitSubjectSizeWiki:
	}

	return nil
}

func assertEvaluation(t *testing.T, rule quota_model.Rule, used quota_model.Used, subject quota_model.LimitSubject, expected bool) {
	t.Helper()

	t.Run(subject.String(), func(t *testing.T) {
		ok, has := rule.Evaluate(used, subject)
		assert.True(t, has)
		assert.Equal(t, expected, ok)
	})
}

func TestQuotaRuleNoEvaluation(t *testing.T) {
	rule := quota_model.Rule{
		Limit: 1024,
		Subjects: quota_model.LimitSubjects{
			quota_model.LimitSubjectSizeAssetsAttachmentsAll,
		},
	}
	used := quota_model.Used{}
	used.Size.Repos.Public = 4096

	_, has := rule.Evaluate(used, quota_model.LimitSubjectSizeReposAll)

	// We have a rule for "size:assets:attachments:all", and query for
	// "size:repos:all". We don't cover that subject, so the evaluation returns
	// with no rules found.
	assert.False(t, has)
}

func TestQuotaRuleDirectEvaluation(t *testing.T) {
	// This function is meant to test direct rule evaluation: cases where we set
	// a rule for a subject, and we evaluate against the same subject.

	runTest := func(t *testing.T, subject quota_model.LimitSubject, limit, used int64, expected bool) {
		t.Helper()

		rule := quota_model.Rule{
			Limit: limit,
			Subjects: quota_model.LimitSubjects{
				subject,
			},
		}
		usedObj := setUsed(quota_model.Used{}, subject, used)
		if usedObj == nil {
			return
		}

		assertEvaluation(t, rule, *usedObj, subject, expected)
	}

	t.Run("limit:0", func(t *testing.T) {
		// With limit:0, nothing used is fine.
		t.Run("used:0", func(t *testing.T) {
			for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
				runTest(t, subject, 0, 0, true)
			}
		})
		// With limit:0, any usage will fail evaluation
		t.Run("used:512", func(t *testing.T) {
			for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
				runTest(t, subject, 0, 512, false)
			}
		})
	})

	t.Run("limit:unlimited", func(t *testing.T) {
		// With no limits, any usage will succeed evaluation
		t.Run("used:512", func(t *testing.T) {
			for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
				runTest(t, subject, -1, 512, true)
			}
		})
	})

	t.Run("limit:1024", func(t *testing.T) {
		// With a set limit, usage below the limit succeeds
		t.Run("used:512", func(t *testing.T) {
			for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
				runTest(t, subject, 1024, 512, true)
			}
		})

		// With a set limit, usage above the limit fails
		t.Run("used:2048", func(t *testing.T) {
			for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
				runTest(t, subject, 1024, 2048, false)
			}
		})
	})
}

func TestQuotaRuleCombined(t *testing.T) {
	rule := quota_model.Rule{
		Limit: 1024,
		Subjects: quota_model.LimitSubjects{
			quota_model.LimitSubjectSizeGitLFS,
			quota_model.LimitSubjectSizeAssetsAttachmentsReleases,
			quota_model.LimitSubjectSizeAssetsPackagesAll,
		},
	}
	used := quota_model.Used{
		Size: quota_model.UsedSize{
			Repos: quota_model.UsedSizeRepos{
				Public: 4096,
			},
			Git: quota_model.UsedSizeGit{
				LFS: 256,
			},
			Assets: quota_model.UsedSizeAssets{
				Attachments: quota_model.UsedSizeAssetsAttachments{
					Issues:   2048,
					Releases: 256,
				},
				Packages: quota_model.UsedSizeAssetsPackages{
					All: 2560,
				},
			},
		},
	}

	expectationMap := map[quota_model.LimitSubject]bool{
		quota_model.LimitSubjectSizeGitLFS:                    false,
		quota_model.LimitSubjectSizeAssetsAttachmentsReleases: false,
		quota_model.LimitSubjectSizeAssetsPackagesAll:         false,
	}

	for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
		t.Run(subject.String(), func(t *testing.T) {
			evalOk, evalHas := rule.Evaluate(used, subject)
			expected, expectedHas := expectationMap[subject]

			assert.Equal(t, expectedHas, evalHas)
			if expectedHas {
				assert.Equal(t, expected, evalOk)
			}
		})
	}
}

func TestQuotaRuleSizeAll(t *testing.T) {
	runTests := func(t *testing.T, rule quota_model.Rule, expected bool) {
		t.Helper()

		subject := quota_model.LimitSubjectSizeAll

		t.Run("used:0", func(t *testing.T) {
			used := quota_model.Used{}

			assertEvaluation(t, rule, used, subject, true)
		})

		t.Run("used:some-each", func(t *testing.T) {
			used := makeFullyUsed()

			assertEvaluation(t, rule, used, subject, expected)
		})

		t.Run("used:some", func(t *testing.T) {
			used := makePartiallyUsed()

			assertEvaluation(t, rule, used, subject, expected)
		})
	}

	// With all limits set to 0, evaluation always fails if usage > 0
	t.Run("rule:0", func(t *testing.T) {
		rule := quota_model.Rule{
			Limit: 0,
			Subjects: quota_model.LimitSubjects{
				quota_model.LimitSubjectSizeAll,
			},
		}

		runTests(t, rule, false)
	})

	// With no limits, evaluation always succeeds
	t.Run("rule:unlimited", func(t *testing.T) {
		rule := quota_model.Rule{
			Limit: -1,
			Subjects: quota_model.LimitSubjects{
				quota_model.LimitSubjectSizeAll,
			},
		}

		runTests(t, rule, true)
	})

	// With a specific, very generous limit, evaluation succeeds if the limit isn't exhausted
	t.Run("rule:generous", func(t *testing.T) {
		rule := quota_model.Rule{
			Limit: 102400,
			Subjects: quota_model.LimitSubjects{
				quota_model.LimitSubjectSizeAll,
			},
		}

		runTests(t, rule, true)

		t.Run("limit exhaustion", func(t *testing.T) {
			used := quota_model.Used{
				Size: quota_model.UsedSize{
					Repos: quota_model.UsedSizeRepos{
						Public: 204800,
					},
				},
			}

			assertEvaluation(t, rule, used, quota_model.LimitSubjectSizeAll, false)
		})
	})

	// With a specific, small limit, evaluation fails
	t.Run("rule:limited", func(t *testing.T) {
		rule := quota_model.Rule{
			Limit: 512,
			Subjects: quota_model.LimitSubjects{
				quota_model.LimitSubjectSizeAll,
			},
		}

		runTests(t, rule, false)
	})
}
