// Copyright (c) 2026 Tigera, 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 outputs

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"sync"
	"testing"
)

func TestRefsWriterAppends(t *testing.T) {
	base := t.TempDir()
	w, err := NewRefsWriter(base, "images-publish")
	if err != nil {
		t.Fatalf("NewRefsWriter: %v", err)
	}
	if err := w.Add("quay.io/calico/node@sha256:aaa"); err != nil {
		t.Fatalf("Add: %v", err)
	}
	if err := w.Add("quay.io/calico/node@sha256:bbb", "quay.io/calico/node@sha256:ccc"); err != nil {
		t.Fatalf("Add: %v", err)
	}

	got, err := ReadRefs(base, "images-publish")
	if err != nil {
		t.Fatalf("ReadRefs: %v", err)
	}
	want := []string{
		"quay.io/calico/node@sha256:aaa",
		"quay.io/calico/node@sha256:bbb",
		"quay.io/calico/node@sha256:ccc",
	}
	if !slices.Equal(got, want) {
		t.Errorf("refs\n got %v\nwant %v", got, want)
	}
}

// A resumed run must add to the record, not replace it.
func TestRefsWriterSurvivesReopen(t *testing.T) {
	base := t.TempDir()
	first, err := NewRefsWriter(base, "images-publish")
	if err != nil {
		t.Fatalf("NewRefsWriter: %v", err)
	}
	if err := first.Add("quay.io/calico/node@sha256:aaa"); err != nil {
		t.Fatalf("Add: %v", err)
	}

	second, err := NewRefsWriter(base, "images-publish")
	if err != nil {
		t.Fatalf("NewRefsWriter (resumed): %v", err)
	}
	if err := second.Add("quay.io/calico/whisker@sha256:bbb"); err != nil {
		t.Fatalf("Add: %v", err)
	}

	got, err := ReadRefs(base, "images-publish")
	if err != nil {
		t.Fatalf("ReadRefs: %v", err)
	}
	want := []string{"quay.io/calico/node@sha256:aaa", "quay.io/calico/whisker@sha256:bbb"}
	if !slices.Equal(got, want) {
		t.Errorf("a resumed run lost the earlier record\n got %v\nwant %v", got, want)
	}
}

// A resumed run re-appends refs it already published.
func TestReadRefsDropsDuplicates(t *testing.T) {
	base := t.TempDir()
	w, err := NewRefsWriter(base, "images-publish")
	if err != nil {
		t.Fatalf("NewRefsWriter: %v", err)
	}
	for range 3 {
		if err := w.Add("quay.io/calico/node@sha256:aaa", "quay.io/calico/whisker@sha256:bbb"); err != nil {
			t.Fatalf("Add: %v", err)
		}
	}

	got, err := ReadRefs(base, "images-publish")
	if err != nil {
		t.Fatalf("ReadRefs: %v", err)
	}
	want := []string{"quay.io/calico/node@sha256:aaa", "quay.io/calico/whisker@sha256:bbb"}
	if !slices.Equal(got, want) {
		t.Errorf("refs\n got %v\nwant %v", got, want)
	}
}

// Components publish concurrently; interleaved writes must not corrupt lines.
func TestRefsWriterConcurrent(t *testing.T) {
	base := t.TempDir()
	w, err := NewRefsWriter(base, "images-publish")
	if err != nil {
		t.Fatalf("NewRefsWriter: %v", err)
	}

	const n = 50
	var wg sync.WaitGroup
	for i := range n {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			if err := w.Add(fmt.Sprintf("quay.io/calico/img@sha256:%03d", i)); err != nil {
				t.Errorf("Add: %v", err)
			}
		}(i)
	}
	wg.Wait()

	got, err := ReadRefs(base, "images-publish")
	if err != nil {
		t.Fatalf("ReadRefs: %v", err)
	}
	if len(got) != n {
		t.Fatalf("expected %d refs, got %d: %v", n, len(got), got)
	}
	for i := range n {
		want := fmt.Sprintf("quay.io/calico/img@sha256:%03d", i)
		if !slices.Contains(got, want) {
			t.Errorf("lost ref %s", want)
		}
	}
}

// A step that has not run is not an error.
func TestReadRefsMissingFile(t *testing.T) {
	got, err := ReadRefs(t.TempDir(), "images-publish")
	if err != nil {
		t.Fatalf("ReadRefs: %v", err)
	}
	if len(got) != 0 {
		t.Errorf("expected no refs, got %v", got)
	}
}

func TestRecordsDir(t *testing.T) {
	out := t.TempDir()
	record := func(t *testing.T, id string) {
		t.Helper()
		w, err := NewRefsWriter(RecordsDir(out, id), "images-publish")
		if err != nil {
			t.Fatalf("NewRefsWriter: %v", err)
		}
		if err := w.Add("quay.io/calico/node:v3.30.0@sha256:aaa"); err != nil {
			t.Fatalf("Add: %v", err)
		}
	}

	t.Run("keeps records out of the upload dir", func(t *testing.T) {
		uploadDir := filepath.Join(out, "release", "v3.30.0")
		if err := os.MkdirAll(uploadDir, 0o755); err != nil {
			t.Fatalf("mkdir: %v", err)
		}
		record(t, "v3.30.0")
		if err := filepath.WalkDir(uploadDir, func(path string, d fs.DirEntry, err error) error {
			if err != nil {
				return err
			}
			if !d.IsDir() && d.Name() == refsFileName {
				t.Errorf("%s is under the upload dir, so it would be published", path)
			}
			return nil
		}); err != nil {
			t.Fatalf("walk: %v", err)
		}
	})

	t.Run("rejects an empty dir", func(t *testing.T) {
		if _, err := ReadRefs("", "images-publish"); err == nil {
			t.Error("ReadRefs: expected an empty records dir to fail")
		}
		if _, err := NewRefsWriter("", "images-publish"); err == nil {
			t.Error("NewRefsWriter: expected an empty records dir to fail")
		}
	})

	t.Run("keeps each release's records apart", func(t *testing.T) {
		record(t, "hash-a")
		got, err := ReadRefs(RecordsDir(out, "hash-b"), "images-publish")
		if err != nil {
			t.Fatalf("ReadRefs: %v", err)
		}
		if len(got) != 0 {
			t.Errorf("another release's records leaked in: %v", got)
		}
	})
}

func TestDigestSourceFor(t *testing.T) {
	const node = "quay.io/calico/node:v3.30.0"
	record := func(t *testing.T, dir, step string, refs ...string) {
		t.Helper()
		w, err := NewRefsWriter(dir, step)
		if err != nil {
			t.Fatalf("NewRefsWriter: %v", err)
		}
		if err := w.Add(refs...); err != nil {
			t.Fatalf("Add: %v", err)
		}
	}

	t.Run("reads the steps in the order given", func(t *testing.T) {
		dir := filepath.Join(t.TempDir(), "upload")
		record(t, dir, "publish", node+"@sha256:aaa")
		record(t, dir, "resolve", node+"@sha256:bbb")
		src, err := DigestSourceFor(dir, "publish", "resolve")
		if err != nil {
			t.Fatalf("DigestSourceFor: %v", err)
		}
		if got, _ := src.Digest(node); got != "sha256:aaa" {
			t.Errorf("Digest = %q, want the publish record's sha256:aaa", got)
		}
	})

	t.Run("a step that has not run misses", func(t *testing.T) {
		src, err := DigestSourceFor(t.TempDir(), "publish")
		if err != nil {
			t.Fatalf("DigestSourceFor: %v", err)
		}
		if got, ok := src.Digest(node); ok {
			t.Errorf("Digest = %q, want a miss", got)
		}
	})

	t.Run("an unreadable record fails", func(t *testing.T) {
		dir := filepath.Join(t.TempDir(), "upload")
		if err := os.MkdirAll(filepath.Join(dir, "publish", refsFileName), 0o755); err != nil {
			t.Fatalf("mkdir: %v", err)
		}
		if _, err := DigestSourceFor(dir, "publish"); err == nil {
			t.Error("expected an unreadable record to fail")
		}
	})
}
