// Copyright 2024 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package find_input_delta_lib

import (
	"errors"
	"io/fs"
	"testing"
	"testing/fstest"
	"time"

	// For Assert*.
	"android/soong/android"

	fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto_internal"
	"google.golang.org/protobuf/proto"
)

// Various state files

func marshalProto(t *testing.T, message proto.Message) []byte {
	data, err := proto.Marshal(message)
	if err != nil {
		t.Errorf("%v", err)
	}
	return data
}

func protoFile(name string, mtime_nsec int64, hash string, contents []*fid_proto.PartialCompileInput) (pci *fid_proto.PartialCompileInput) {
	pci = &fid_proto.PartialCompileInput{
		Name: proto.String(name),
	}
	if mtime_nsec != 0 {
		pci.MtimeNsec = proto.Int64(mtime_nsec)
	}
	if len(hash) > 0 {
		pci.Hash = proto.String(hash)
	}
	if contents != nil {
		pci.Contents = contents
	}
	return
}

func TestLoadState(t *testing.T) {
	testCases := []struct {
		Name     string
		Filename string
		Mapfs    fs.ReadFileFS
		Expected *fid_proto.PartialCompileInputs
		Err      error
	}{
		{
			Name:     "missing file",
			Filename: "missing",
			Mapfs:    fstest.MapFS{},
			Expected: &fid_proto.PartialCompileInputs{},
			Err:      nil,
		},
		{
			Name:     "bad file",
			Filename: ".",
			Mapfs:    OsFs,
			Expected: &fid_proto.PartialCompileInputs{},
			Err:      errors.New("read failed"),
		},
		{
			Name:     "file with mtime",
			Filename: "state.old",
			Mapfs: fstest.MapFS{
				"state.old": &fstest.MapFile{
					Data: marshalProto(t, &fid_proto.PartialCompileInputs{
						InputFiles: []*fid_proto.PartialCompileInput{
							protoFile("input1", 100, "", nil),
						},
					}),
				},
			},
			Expected: &fid_proto.PartialCompileInputs{
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("input1", 100, "", nil),
				},
			},
			Err: nil,
		},
		{
			Name:     "file with mtime and hash",
			Filename: "state.old",
			Mapfs: fstest.MapFS{
				"state.old": &fstest.MapFile{
					Data: marshalProto(t, &fid_proto.PartialCompileInputs{
						InputFiles: []*fid_proto.PartialCompileInput{
							protoFile("input1", 100, "crc:crc_value", nil),
						},
					}),
				},
			},
			Expected: &fid_proto.PartialCompileInputs{
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("input1", 100, "crc:crc_value", nil),
				},
			},
			Err: nil,
		},
	}
	for _, tc := range testCases {
		actual, err := loadState(tc.Filename, tc.Mapfs)
		if tc.Err == nil {
			android.AssertSame(t, tc.Name, tc.Err, err)
		} else if err == nil {
			t.Errorf("%s: expected error, did not get one", tc.Name)
		}
		if !proto.Equal(tc.Expected, actual) {
			t.Errorf("%s: expected %v, actual %v", tc.Name, tc.Expected, actual)
		}
	}
}

func TestCreateState(t *testing.T) {
	testCases := []struct {
		Name     string
		Version  string
		Tools    []string
		Inputs   []string
		Inspect  bool
		Mapfs    StatReadFileFS
		Expected *fid_proto.PartialCompileInputs
		Err      error
	}{
		{
			Name:     "no inputs",
			Inputs:   []string{},
			Mapfs:    fstest.MapFS{},
			Expected: &fid_proto.PartialCompileInputs{},
			Err:      nil,
		},
		{
			Name:    "files found",
			Version: "version",
			Tools:   []string{"tool"},
			Inputs:  []string{"baz", "foo", "bar"},
			Mapfs: fstest.MapFS{
				"tool": &fstest.MapFile{ModTime: time.Unix(0, 10000).UTC()},
				"foo":  &fstest.MapFile{ModTime: time.Unix(0, 100).UTC()},
				"baz":  &fstest.MapFile{ModTime: time.Unix(0, 300).UTC()},
				"bar":  &fstest.MapFile{ModTime: time.Unix(0, 200).UTC()},
			},
			Expected: &fid_proto.PartialCompileInputs{
				Version: proto.String("version"),
				Tools: []*fid_proto.PartialCompileInput{
					protoFile("tool", 10000, "", nil),
				},
				InputFiles: []*fid_proto.PartialCompileInput{
					// Files are always sorted.
					protoFile("bar", 200, "", nil),
					protoFile("baz", 300, "", nil),
					protoFile("foo", 100, "", nil),
				},
			},
			Err: nil,
		},
	}
	for _, tc := range testCases {
		actual, err := createState(tc.Version, tc.Tools, tc.Inputs, tc.Inspect, tc.Mapfs)
		if tc.Err == nil {
			android.AssertSame(t, tc.Name, tc.Err, err)
		} else if err == nil {
			t.Errorf("%s: expected error, did not get one", tc.Name)
		}
		if !proto.Equal(tc.Expected, actual) {
			t.Errorf("%s: expected %v, actual %v", tc.Name, tc.Expected, actual)
		}
	}
}

func TestCompareInternalState(t *testing.T) {
	testCases := []struct {
		Name     string
		Target   string
		Prior    *fid_proto.PartialCompileInputs
		New      *fid_proto.PartialCompileInputs
		Expected *FileList
	}{
		{
			Name:   "prior is empty",
			Target: "foo",
			Prior:  &fid_proto.PartialCompileInputs{},
			New: &fid_proto.PartialCompileInputs{
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("file1", 100, "", nil),
				},
			},
			Expected: &FileList{
				Name:      "foo",
				Additions: []string{"file1"},
			},
		},
		{
			Name:   "version changed",
			Target: "foo",
			Prior: &fid_proto.PartialCompileInputs{
				Version: proto.String("1.0"),
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("file0", 100, "", nil),
					protoFile("file1", 100, "", nil),
					protoFile("file2", 200, "", nil),
				},
			},
			New: &fid_proto.PartialCompileInputs{
				Version: proto.String("1.1"),
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("file0", 100, "", nil),
					protoFile("file1", 100, "", nil),
					protoFile("file2", 200, "", nil),
				},
			},
			Expected: &FileList{
				Name:      "foo",
				Additions: []string{"file0", "file1", "file2"},
			},
		},
		{
			Name:   "tools updated",
			Target: "foo",
			Prior: &fid_proto.PartialCompileInputs{
				Version: proto.String("1.0"),
				Tools: []*fid_proto.PartialCompileInput{
					protoFile("tool", 50, "", nil),
				},
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("file0", 100, "", nil),
					protoFile("file1", 100, "", nil),
					protoFile("file2", 200, "", nil),
				},
			},
			New: &fid_proto.PartialCompileInputs{
				Version: proto.String("1.0"),
				Tools: []*fid_proto.PartialCompileInput{
					protoFile("tool", 1000, "", nil),
				},
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("file0", 100, "", nil),
					protoFile("file1", 100, "", nil),
					protoFile("file2", 200, "", nil),
				},
			},
			Expected: &FileList{
				Name:      "foo",
				Additions: []string{"file0", "file1", "file2"},
			},
		},
		{
			Name:   "one each add modify delete",
			Target: "foo",
			Prior: &fid_proto.PartialCompileInputs{
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("file0", 100, "", nil),
					protoFile("file1", 100, "", nil),
					protoFile("file2", 200, "", nil),
				},
			},
			New: &fid_proto.PartialCompileInputs{
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("file0", 100, "", nil),
					protoFile("file1", 200, "", nil),
					protoFile("file3", 300, "", nil),
				},
			},
			Expected: &FileList{
				Name:      "foo",
				Additions: []string{"file3"},
				Changes:   []FileList{FileList{Name: "file1"}},
				Deletions: []string{"file2"},
			},
		},
		{
			Name:   "interior one each add modify delete",
			Target: "bar",
			Prior: &fid_proto.PartialCompileInputs{
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("file1", 405, "", []*fid_proto.PartialCompileInput{
						protoFile("innerC", 400, "crc32:11111111", nil),
						protoFile("innerD", 400, "crc32:44444444", nil),
					}),
				},
			},
			New: &fid_proto.PartialCompileInputs{
				InputFiles: []*fid_proto.PartialCompileInput{
					protoFile("file1", 505, "", []*fid_proto.PartialCompileInput{
						protoFile("innerA", 400, "crc32:55555555", nil),
						protoFile("innerC", 500, "crc32:66666666", nil),
					}),
				},
			},
			Expected: &FileList{
				Name: "bar",
				Changes: []FileList{FileList{
					Name:      "file1",
					Additions: []string{"innerA"},
					Changes:   []FileList{FileList{Name: "innerC"}},
					Deletions: []string{"innerD"},
				}},
			},
		},
	}
	for _, tc := range testCases {
		actual := compareInternalState(tc.Prior, tc.New, tc.Target)
		if !tc.Expected.Equal(actual) {
			t.Errorf("%s: expected %v, actual %v", tc.Name, tc.Expected, actual)
		}
	}
}

func TestCompareInspectExtsZipRegexp(t *testing.T) {
	testCases := []struct {
		Name     string
		Expected bool
	}{
		{Name: ".jar", Expected: true},
		{Name: ".jar5", Expected: true},
		{Name: ".apex", Expected: true},
		{Name: ".apex9", Expected: true},
		{Name: ".apexx", Expected: false},
		{Name: ".apk", Expected: true},
		{Name: ".apk3", Expected: true},
		{Name: ".go", Expected: false},
	}
	for _, tc := range testCases {
		actual := InspectExtsZipRegexp.Match([]byte(tc.Name))
		if tc.Expected != actual {
			t.Errorf("%s: expected %v, actual %v", tc.Name, tc.Expected, actual)
		}
	}
}
