package repository

import (
	"context"
	"fmt"
	"os"
	"sync"
	"testing"

	"github.com/restic/restic/internal/backend"
	"github.com/restic/restic/internal/backend/local"
	"github.com/restic/restic/internal/backend/mem"
	"github.com/restic/restic/internal/backend/retry"
	"github.com/restic/restic/internal/crypto"
	"github.com/restic/restic/internal/restic"
	"github.com/restic/restic/internal/test"

	"github.com/restic/chunker"
)

type logger interface {
	Logf(format string, args ...interface{})
}

var paramsOnce sync.Once

// TestUseLowSecurityKDFParameters configures low-security KDF parameters for testing.
func TestUseLowSecurityKDFParameters(t logger) {
	t.Logf("using low-security KDF parameters for test")
	paramsOnce.Do(func() {
		params = &crypto.Params{
			N: 128,
			R: 1,
			P: 1,
		}
	})
}

// TestBackend returns a fully configured in-memory backend.
func TestBackend(_ testing.TB) backend.Backend {
	return mem.New()
}

const testChunkerPol = chunker.Pol(0x3DA3358B4DC173)

// TestRepositoryWithBackend returns a repository initialized with a test
// password. If be is nil, an in-memory backend is used. A constant polynomial
// is used for the chunker and low-security test parameters.
func TestRepositoryWithBackend(t testing.TB, be backend.Backend, version uint, opts Options) (*Repository, backend.Backend) {
	t.Helper()
	TestUseLowSecurityKDFParameters(t)
	restic.TestDisableCheckPolynomial(t)

	if be == nil {
		be = TestBackend(t)
	}

	repo, err := New(be, opts)
	if err != nil {
		t.Fatalf("TestRepository(): new repo failed: %v", err)
	}

	if version == 0 {
		version = restic.StableRepoVersion
	}
	pol := testChunkerPol
	err = repo.Init(context.TODO(), version, test.TestPassword, &pol)
	if err != nil {
		t.Fatalf("TestRepository(): initialize repo failed: %v", err)
	}

	return repo, be
}

// TestRepository returns a repository initialized with a test password on an
// in-memory backend. When the environment variable RESTIC_TEST_REPO is set to
// a non-existing directory, a local backend is created there and this is used
// instead. The directory is not removed, but left there for inspection.
func TestRepository(t testing.TB) *Repository {
	t.Helper()
	repo, _, _ := TestRepositoryWithVersion(t, 0)
	return repo
}

func TestRepositoryWithVersion(t testing.TB, version uint) (*Repository, restic.Unpacked[restic.FileType], backend.Backend) {
	t.Helper()
	dir := os.Getenv("RESTIC_TEST_REPO")
	opts := Options{}
	var repo *Repository
	var be backend.Backend
	if dir != "" {
		_, err := os.Stat(dir)
		if err != nil {
			lbe, err := local.Create(context.TODO(), local.Config{Path: dir}, t.Logf)
			if err != nil {
				t.Fatalf("error creating local backend at %v: %v", dir, err)
			}
			repo, be = TestRepositoryWithBackend(t, lbe, version, opts)
		} else {
			t.Logf("directory at %v already exists, using mem backend", dir)
		}
	} else {
		repo, be = TestRepositoryWithBackend(t, nil, version, opts)
	}
	return repo, &internalRepository{repo}, be
}

func TestFromFixture(t testing.TB, repoFixture string) (*Repository, backend.Backend, func()) {
	repodir, cleanup := test.Env(t, repoFixture)
	repo, be := TestOpenLocal(t, repodir)

	return repo, be, cleanup
}

// TestOpenLocal opens a local repository.
func TestOpenLocal(t testing.TB, dir string) (*Repository, backend.Backend) {
	var be backend.Backend
	be, err := local.Open(context.TODO(), local.Config{Path: dir, Connections: 2}, t.Logf)
	if err != nil {
		t.Fatal(err)
	}

	be = retry.New(be, 3, nil, nil)

	return TestOpenBackend(t, be), be
}

func TestOpenBackend(t testing.TB, be backend.Backend) *Repository {
	repo, err := New(be, Options{})
	if err != nil {
		t.Fatal(err)
	}
	err = repo.SearchKey(context.TODO(), test.TestPassword, 10, "")
	if err != nil {
		t.Fatal(err)
	}

	return repo
}

type VersionedTest func(t *testing.T, version uint)

func TestAllVersions(t *testing.T, test VersionedTest) {
	for version := restic.MinRepoVersion; version <= restic.MaxRepoVersion; version++ {
		t.Run(fmt.Sprintf("v%d", version), func(t *testing.T) {
			test(t, uint(version))
		})
	}
}

type VersionedBenchmark func(b *testing.B, version uint)

func BenchmarkAllVersions(b *testing.B, bench VersionedBenchmark) {
	for version := restic.MinRepoVersion; version <= restic.MaxRepoVersion; version++ {
		b.Run(fmt.Sprintf("v%d", version), func(b *testing.B) {
			bench(b, uint(version))
		})
	}
}

func TestNewLock(_ *testing.T, repo *Repository, exclusive bool) (*restic.Lock, error) {
	// TODO get rid of this test helper
	return restic.NewLock(context.TODO(), &internalRepository{repo}, exclusive)
}

// TestCheckRepo runs the checker on repo.
func TestCheckRepo(t testing.TB, repo *Repository) {
	chkr := NewChecker(repo)

	hints, errs := chkr.LoadIndex(context.TODO(), nil)
	if len(errs) != 0 {
		t.Fatalf("errors loading index: %v", errs)
	}

	if len(hints) != 0 {
		t.Fatalf("errors loading index: %v", hints)
	}

	// packs
	errChan := make(chan error)
	go chkr.Packs(context.TODO(), errChan)

	for err := range errChan {
		t.Error(err)
	}

	// read data
	errChan = make(chan error)
	go chkr.ReadPacks(context.TODO(), func(packs map[restic.ID]int64) map[restic.ID]int64 {
		return packs
	}, nil, errChan)

	for err := range errChan {
		t.Error(err)
	}
}
