//go:build darwin || linux

package mmap

import (
	"bytes"
	"crypto"
	"io"
	"testing"

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

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

var _ plumbing.EncodedObject = (*ondemandObject)(nil)

var data = []struct {
	hash       plumbing.Hash
	content    []byte
	rawContent []byte
}{
	{
		hash:       plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"),
		content:    []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},
		rawContent: []byte{0x80, 0x2e, 0x78, 0x9c, 0xfb, 0xc7, 0xf8, 0x95, 0x51, 0xb7, 0xa4, 0x28, 0x35, 0x55, 0x21, 0xd1, 0x22, 0xc5, 0xd8, 0xd0, 0x34, 0xc9, 0x28, 0xc9, 0x30, 0xd9, 0xcc, 0xd0, 0x34, 0xc5, 0xc4, 0xd8, 0xc0, 0xc4, 0x28, 0xd9, 0x38, 0xd1, 0xcc, 0xc8, 0xc4, 0xc0, 0x28, 0xc9, 0x22, 0xd1, 0xd4, 0xc4, 0xc8, 0xc2, 0x22, 0x39, 0xcd, 0x34, 0x79, 0xa2, 0x6e, 0x1a, 0xbb, 0x85, 0x91, 0x99, 0xa5, 0x89, 0x89, 0xf9, 0xc4, 0x59, 0xf6, 0x32, 0x50, 0xa6, 0x82, 0xb6, 0x81, 0x91, 0x81, 0x1, 0x17, 0x57, 0x59, 0x6a, 0x5e, 0x4a, 0x7e, 0x91, 0x42, 0x71, 0x49, 0x69, 0x5a, 0x1a, 0x17},
	},
	{
		hash:    plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"),
		content: []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},
	},
}

func TestOndemandObject(t *testing.T) {
	t.Parallel()
	fixture := fixtures.NewOSFixture(fixtures.Basic().One(), t.TempDir())
	pack, err := fixture.Packfile()
	require.NoError(t, err)
	idx, err := fixture.Idx()
	require.NoError(t, err)
	rev, err := fixture.Rev()
	require.NoError(t, err)
	scanner, err := NewPackScanner(crypto.SHA1.Size(), pack, idx, rev)
	require.NoError(t, err)
	t.Cleanup(func() {
		err := scanner.Close()
		require.NoError(t, err)
	})

	tests := []struct {
		name             string
		hash             plumbing.Hash
		autoResolve      bool
		wantSize         int64
		wantType         plumbing.ObjectType
		wantResolvedSize int64
		wantResolvedType plumbing.ObjectType
		wantContent      []byte
	}{
		{
			name:             "delta commit (autoResolve: true)",
			hash:             data[0].hash,
			autoResolve:      true,
			wantSize:         245,
			wantType:         plumbing.CommitObject,
			wantResolvedSize: 245,
			wantResolvedType: plumbing.CommitObject,
			wantContent:      data[0].content,
		},
		{
			name:             "delta commit (autoResolve: false)",
			hash:             data[0].hash,
			autoResolve:      false,
			wantSize:         93,
			wantType:         plumbing.OFSDeltaObject,
			wantResolvedSize: 245,
			wantResolvedType: plumbing.CommitObject,
			wantContent:      data[0].rawContent,
		},
		{
			name:             "non-delta commit (autoResolve: true)",
			hash:             data[1].hash,
			autoResolve:      true,
			wantSize:         254,
			wantType:         plumbing.CommitObject,
			wantResolvedSize: 254,
			wantResolvedType: plumbing.CommitObject,
			wantContent:      data[1].content,
		},
		{
			name:             "non-delta commit (autoResolve: false)",
			hash:             plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"),
			autoResolve:      false,
			wantSize:         254,
			wantType:         plumbing.CommitObject,
			wantResolvedSize: 254,
			wantResolvedType: plumbing.CommitObject,
			wantContent:      data[1].content,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			offset, err := scanner.FindOffset(tc.hash)
			require.NoError(t, err)

			typ := scanner.packMmap[offset]
			size, err := packutil.VariableLengthSize(typ, bytes.NewReader(scanner.packMmap[offset+1:]))
			require.NoError(t, err)

			obj := newOndemandObject(tc.hash, packutil.ObjectType(typ), int64(offset), int64(size), scanner, tc.autoResolve)

			assert.Equal(t, tc.hash, obj.Hash())
			assert.Equal(t, tc.wantType, obj.Type(), "type mismatch")
			assert.Equal(t, tc.wantSize, obj.Size(), "size mismatch")

			// Verify content before calling Resolve(), as in some cases the result will differ.
			if tc.wantContent != nil {
				r, err := obj.Reader()
				require.NoError(t, err)
				defer r.Close()

				content, err := io.ReadAll(r)
				require.NoError(t, err)
				assert.Equal(t, tc.wantContent, content, "content mismatch")
			}

			err = obj.Resolve()
			require.NoError(t, err)
			assert.Equal(t, tc.wantResolvedType, obj.Type(), "resolved type mismatch")
			assert.Equal(t, tc.wantResolvedSize, obj.Size(), "resolved size mismatch")
		})
	}
}
