package pack_test

import (
	"io"
	"testing"

	"github.com/go-git/go-billy/v6/osfs"
	fixtures "github.com/go-git/go-git-fixtures/v6"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	"github.com/go-git/go-git/v6/plumbing"
	"github.com/go-git/go-git/v6/plumbing/cache"
	"github.com/go-git/go-git/v6/plumbing/format/packfile"
)

type PackHandlerSuite[T int64OrUint64] struct {
	suite.Suite
	newPackHandler func() packHandler[T]
}

func TestPackfileEmbedFS(t *testing.T) {
	t.Parallel()
	fixture := fixtures.Basic().One()
	suite.Run(t, &PackHandlerSuite[int64]{
		newPackHandler: func() packHandler[int64] {
			pack, err := fixture.Packfile()
			require.NoError(t, err)
			idx, err := fixture.Idx()
			require.NoError(t, err)
			t.Cleanup(func() {
				pack.Close()
				idx.Close()
			})

			return newPackfileOpts(pack, idx,
				packfile.WithFs(osfs.New(t.TempDir())),
				packfile.WithCache(cache.NewObjectLRUDefault()))
		},
	})
}

func TestPackfileWithFSandCache(t *testing.T) {
	t.Parallel()
	fixture := fixtures.NewOSFixture(fixtures.Basic().One(), t.TempDir())
	suite.Run(t, &PackHandlerSuite[int64]{
		newPackHandler: func() packHandler[int64] {
			pack, err := fixture.Packfile()
			require.NoError(t, err)
			idx, err := fixture.Idx()
			require.NoError(t, err)
			t.Cleanup(func() {
				pack.Close()
				idx.Close()
			})

			return newPackfileOpts(pack, idx,
				packfile.WithFs(osfs.New(t.TempDir())),
				packfile.WithCache(cache.NewObjectLRUDefault()))
		},
	})
}

func TestPackfileWithFS(t *testing.T) {
	t.Parallel()
	fixture := fixtures.NewOSFixture(fixtures.Basic().One(), t.TempDir())
	suite.Run(t, &PackHandlerSuite[int64]{
		newPackHandler: func() packHandler[int64] {
			pack, err := fixture.Packfile()
			require.NoError(t, err)
			idx, err := fixture.Idx()
			require.NoError(t, err)
			t.Cleanup(func() {
				pack.Close()
				idx.Close()
			})

			return newPackfileOpts(pack, idx, packfile.WithFs(osfs.New(t.TempDir())))
		},
	})
}

func TestPackfileWithCache(t *testing.T) {
	t.Parallel()
	fixture := fixtures.NewOSFixture(fixtures.Basic().One(), t.TempDir())
	suite.Run(t, &PackHandlerSuite[int64]{
		newPackHandler: func() packHandler[int64] {
			pack, err := fixture.Packfile()
			require.NoError(t, err)
			idx, err := fixture.Idx()
			require.NoError(t, err)
			t.Cleanup(func() {
				pack.Close()
				idx.Close()
			})

			return newPackfileOpts(pack, idx,
				packfile.WithCache(cache.NewObjectLRUDefault()))
		},
	})
}

func (p *PackHandlerSuite[T]) TestFindHash() {
	handler := p.newPackHandler()
	defer handler.Close()

	tests := []struct {
		name    string
		offset  uint64
		want    plumbing.ObjectID
		wantErr string
	}{
		{
			name:    "invalid offset - not found",
			offset:  999999,
			wantErr: "not found",
		},
		{
			name:    "zero offset",
			offset:  0,
			wantErr: "not found",
		},
		{
			name:   "valid commit hash",
			offset: 12,
			want:   plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"),
		},
		{
			name:   "valid delta commit hash (1 level)",
			offset: 186,
			want:   plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"),
		},
		{
			name:   "valid blob hash",
			offset: 1524,
			want:   plumbing.NewHash("32858aad3c383ed1ff0a0f9bdf231d54a00c9e88"),
		},
		{
			name:   "valid tree hash",
			offset: 84479,
			want:   plumbing.NewHash("5a877e6a906a2743ad6e45d99c1793642aaf8eda"),
		},
		{
			name:   "valid delta tree hash (1 level)",
			offset: 84375,
			want:   plumbing.NewHash("a8d315b2b1c615d43042c3a62402b8a54288cf5c"),
		},
		{
			name:   "valid delta tree hash (3 levels)",
			offset: 84760,
			want:   plumbing.NewHash("aa9b383c260e1d05fbbf6b30a02914555e20c725"),
		},
	}

	for _, tc := range tests {
		p.T().Run(tc.name, func(t *testing.T) {
			hash, err := handler.FindHash(T(tc.offset))
			if tc.wantErr != "" {
				assert.ErrorContains(t, err, tc.wantErr)
				assert.Equal(t, plumbing.ZeroHash, hash)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tc.want, hash)
			}
		})
	}
}

func (p *PackHandlerSuite[T]) TestFindOffset() {
	handler := p.newPackHandler()
	defer handler.Close()

	tests := []struct {
		name    string
		hash    plumbing.Hash
		want    T
		wantErr string
	}{
		{
			name:    "zero hash - not found",
			hash:    plumbing.ZeroHash,
			wantErr: "not found",
		},
		{
			name:    "inexistent hash - not found",
			hash:    plumbing.NewHash("ffffffffffffffffffffffffffffffffffffffff"),
			wantErr: "not found",
		},
		{
			name:    "wrong sized hash - not found",
			hash:    plumbing.NewHash("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
			wantErr: "not found",
		},
		{
			name: "valid commit hash",
			hash: plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"),
			want: 12,
		},
		{
			name: "valid delta commit hash (1 level)",
			hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"),
			want: 186,
		},
		{
			name: "valid blob hash",
			hash: plumbing.NewHash("32858aad3c383ed1ff0a0f9bdf231d54a00c9e88"),
			want: 1524,
		},
		{
			name: "valid tree hash",
			hash: plumbing.NewHash("5a877e6a906a2743ad6e45d99c1793642aaf8eda"),
			want: 84479,
		},
		{
			name: "valid delta tree hash (1 level)",
			hash: plumbing.NewHash("a8d315b2b1c615d43042c3a62402b8a54288cf5c"),
			want: 84375,
		},
		{
			name: "valid delta tree hash (3 levels)",
			hash: plumbing.NewHash("aa9b383c260e1d05fbbf6b30a02914555e20c725"),
			want: 84760,
		},
	}

	for _, tc := range tests {
		p.T().Run(tc.name, func(t *testing.T) {
			offset, err := handler.FindOffset(tc.hash)
			if tc.wantErr != "" {
				assert.ErrorContains(t, err, tc.wantErr)
				assert.Equal(t, T(0), offset)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tc.want, offset)
			}
		})
	}
}

func (p *PackHandlerSuite[T]) TestGet() {
	handler := p.newPackHandler()
	defer handler.Close()

	tests := []struct {
		name        string
		hash        plumbing.Hash
		wantSize    int64
		wantType    plumbing.ObjectType
		wantContent []byte
		wantErr     string
	}{
		{
			name:        "valid commit hash",
			hash:        plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"),
			wantSize:    254,
			wantType:    plumbing.CommitObject,
			wantContent: []byte{0x74, 0x72, 0x65, 0x65, 0x20, 0x64, 0x62, 0x64, 0x33, 0x36, 0x34, 0x31, 0x62, 0x33, 0x37, 0x31, 0x30, 0x32, 0x34, 0x66, 0x34, 0x34, 0x64, 0x30, 0x65, 0x34, 0x36, 0x39, 0x61, 0x39, 0x63, 0x38, 0x66, 0x35, 0x34, 0x35, 0x37, 0x62, 0x30, 0x36, 0x36, 0x30, 0x64, 0x65, 0x31, 0xa, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x20, 0x39, 0x31, 0x38, 0x63, 0x34, 0x38, 0x62, 0x38, 0x33, 0x62, 0x64, 0x30, 0x38, 0x31, 0x65, 0x38, 0x36, 0x33, 0x64, 0x62, 0x65, 0x31, 0x62, 0x38, 0x30, 0x66, 0x38, 0x39, 0x39, 0x38, 0x66, 0x30, 0x35, 0x38, 0x63, 0x64, 0x38, 0x32, 0x39, 0x34, 0xa, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x20, 0x4d, 0xc3, 0xa1, 0x78, 0x69, 0x6d, 0x6f, 0x20, 0x43, 0x75, 0x61, 0x64, 0x72, 0x6f, 0x73, 0x20, 0x4f, 0x72, 0x74, 0x69, 0x7a, 0x20, 0x3c, 0x6d, 0x63, 0x75, 0x61, 0x64, 0x72, 0x6f, 0x73, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x3e, 0x20, 0x31, 0x34, 0x32, 0x37, 0x38, 0x30, 0x33, 0x32, 0x30, 0x38, 0x20, 0x2b, 0x30, 0x32, 0x30, 0x30, 0xa, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x20, 0x4d, 0xc3, 0xa1, 0x78, 0x69, 0x6d, 0x6f, 0x20, 0x43, 0x75, 0x61, 0x64, 0x72, 0x6f, 0x73, 0x20, 0x4f, 0x72, 0x74, 0x69, 0x7a, 0x20, 0x3c, 0x6d, 0x63, 0x75, 0x61, 0x64, 0x72, 0x6f, 0x73, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x3e, 0x20, 0x31, 0x34, 0x32, 0x37, 0x38, 0x30, 0x33, 0x32, 0x30, 0x38, 0x20, 0x2b, 0x30, 0x32, 0x30, 0x30, 0xa, 0xa, 0x73, 0x6f, 0x6d, 0x65, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0xa},
		},
		{
			name:        "valid delta commit hash (1 level)",
			hash:        plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"),
			wantSize:    245,
			wantType:    plumbing.CommitObject,
			wantContent: []byte{0x74, 0x72, 0x65, 0x65, 0x20, 0x61, 0x38, 0x64, 0x33, 0x31, 0x35, 0x62, 0x32, 0x62, 0x31, 0x63, 0x36, 0x31, 0x35, 0x64, 0x34, 0x33, 0x30, 0x34, 0x32, 0x63, 0x33, 0x61, 0x36, 0x32, 0x34, 0x30, 0x32, 0x62, 0x38, 0x61, 0x35, 0x34, 0x32, 0x38, 0x38, 0x63, 0x66, 0x35, 0x63, 0xa, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x20, 0x39, 0x31, 0x38, 0x63, 0x34, 0x38, 0x62, 0x38, 0x33, 0x62, 0x64, 0x30, 0x38, 0x31, 0x65, 0x38, 0x36, 0x33, 0x64, 0x62, 0x65, 0x31, 0x62, 0x38, 0x30, 0x66, 0x38, 0x39, 0x39, 0x38, 0x66, 0x30, 0x35, 0x38, 0x63, 0x64, 0x38, 0x32, 0x39, 0x34, 0xa, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x20, 0x4d, 0xc3, 0xa1, 0x78, 0x69, 0x6d, 0x6f, 0x20, 0x43, 0x75, 0x61, 0x64, 0x72, 0x6f, 0x73, 0x20, 0x4f, 0x72, 0x74, 0x69, 0x7a, 0x20, 0x3c, 0x6d, 0x63, 0x75, 0x61, 0x64, 0x72, 0x6f, 0x73, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x3e, 0x20, 0x31, 0x34, 0x32, 0x38, 0x32, 0x36, 0x39, 0x34, 0x34, 0x37, 0x20, 0x2b, 0x30, 0x32, 0x30, 0x30, 0xa, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x20, 0x4d, 0xc3, 0xa1, 0x78, 0x69, 0x6d, 0x6f, 0x20, 0x43, 0x75, 0x61, 0x64, 0x72, 0x6f, 0x73, 0x20, 0x4f, 0x72, 0x74, 0x69, 0x7a, 0x20, 0x3c, 0x6d, 0x63, 0x75, 0x61, 0x64, 0x72, 0x6f, 0x73, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x3e, 0x20, 0x31, 0x34, 0x32, 0x38, 0x32, 0x36, 0x39, 0x34, 0x34, 0x37, 0x20, 0x2b, 0x30, 0x32, 0x30, 0x30, 0xa, 0xa, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x20, 0x73, 0x74, 0x75, 0x66, 0x66, 0xa},
		},
		{
			name:        "valid blob hash",
			hash:        plumbing.NewHash("32858aad3c383ed1ff0a0f9bdf231d54a00c9e88"),
			wantSize:    189,
			wantType:    plumbing.BlobObject,
			wantContent: []byte{0x2a, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x73, 0xa, 0xa, 0x23, 0x20, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x20, 0x54, 0x6f, 0x6f, 0x6c, 0x73, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x4a, 0x61, 0x76, 0x61, 0x20, 0x28, 0x4a, 0x32, 0x4d, 0x45, 0x29, 0xa, 0x2e, 0x6d, 0x74, 0x6a, 0x2e, 0x74, 0x6d, 0x70, 0x2f, 0xa, 0xa, 0x23, 0x20, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x20, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x20, 0x23, 0xa, 0x2a, 0x2e, 0x6a, 0x61, 0x72, 0xa, 0x2a, 0x2e, 0x77, 0x61, 0x72, 0xa, 0x2a, 0x2e, 0x65, 0x61, 0x72, 0xa, 0xa, 0x23, 0x20, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x20, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x20, 0x63, 0x72, 0x61, 0x73, 0x68, 0x20, 0x6c, 0x6f, 0x67, 0x73, 0x2c, 0x20, 0x73, 0x65, 0x65, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x6e, 0x2f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x2f, 0x68, 0x65, 0x6c, 0x70, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x68, 0x6f, 0x74, 0x73, 0x70, 0x6f, 0x74, 0x2e, 0x78, 0x6d, 0x6c, 0xa, 0x68, 0x73, 0x5f, 0x65, 0x72, 0x72, 0x5f, 0x70, 0x69, 0x64, 0x2a, 0xa},
		},
		{
			name:        "valid tree hash",
			hash:        plumbing.NewHash("5a877e6a906a2743ad6e45d99c1793642aaf8eda"),
			wantSize:    75,
			wantType:    plumbing.TreeObject,
			wantContent: []byte{0x31, 0x30, 0x30, 0x36, 0x34, 0x34, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x2e, 0x6a, 0x73, 0x6f, 0x6e, 0x0, 0x49, 0xc6, 0xbb, 0x89, 0xb1, 0x70, 0x60, 0xd7, 0xb4, 0xde, 0xac, 0xb7, 0xb3, 0x38, 0xfc, 0xc6, 0xea, 0x23, 0x52, 0xa9, 0x31, 0x30, 0x30, 0x36, 0x34, 0x34, 0x20, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x2e, 0x6a, 0x73, 0x6f, 0x6e, 0x0, 0xc8, 0xf1, 0xd8, 0xc6, 0x1f, 0x9d, 0xa7, 0x6f, 0x4c, 0xb4, 0x9f, 0xd8, 0x63, 0x22, 0xb6, 0xe6, 0x85, 0xdb, 0xa9, 0x56},
		},
		{
			name:        "valid delta tree hash (3 levels)",
			hash:        plumbing.NewHash("aa9b383c260e1d05fbbf6b30a02914555e20c725"),
			wantSize:    73,
			wantType:    plumbing.TreeObject,
			wantContent: []byte{0x31, 0x30, 0x30, 0x36, 0x34, 0x34, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x0, 0x32, 0x85, 0x8a, 0xad, 0x3c, 0x38, 0x3e, 0xd1, 0xff, 0xa, 0xf, 0x9b, 0xdf, 0x23, 0x1d, 0x54, 0xa0, 0xc, 0x9e, 0x88, 0x31, 0x30, 0x30, 0x36, 0x34, 0x34, 0x20, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x0, 0xc1, 0x92, 0xbd, 0x6a, 0x24, 0xea, 0x1a, 0xb0, 0x1d, 0x78, 0x68, 0x6e, 0x41, 0x7c, 0x8b, 0xdc, 0x7c, 0x3d, 0x19, 0x7f},
		},
	}

	for _, tc := range tests {
		p.T().Run(tc.name, func(t *testing.T) {
			obj, err := handler.Get(tc.hash)
			if tc.wantErr != "" {
				assert.ErrorContains(t, err, tc.wantErr)
				assert.Nil(t, obj)
			} else {
				require.NoError(t, err)
				assert.NotNil(t, obj)

				assert.Equal(t, tc.hash, obj.Hash())
				assert.Equal(t, tc.wantSize, obj.Size())
				assert.Equal(t, tc.wantType, obj.Type())

				r, err := obj.Reader()
				require.NoError(t, err)
				content, err := io.ReadAll(r)
				require.NoError(t, err)

				assert.Equal(t, tc.wantContent, content)
			}
		})
	}
}
