package automation

import (
	"archive/tar"
	"compress/gzip"
	"context"
	"io"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/service/automation/runtime"
)

func TestAGSAutomationCheckoutMaterializerExtractsArchive(t *testing.T) {
	destination := automationCheckoutTempDir(t)
	entries := []automationCheckoutTestEntry{
		{name: "go.mod", body: "module example.test/repo\n"},
		{name: "cmd/app/main.go", body: "package main\n"},
		{name: "bin/tool", body: "#!/bin/sh\n", mode: 0o755},
		{name: "docs/readme-link", link: "../README.md"},
	}
	materializer := AGSAutomationCheckoutMaterializer{Archiver: fakeAutomationRepositoryArchiver{write: func(w io.Writer) error {
		return writeAutomationCheckoutTestArchive(w, entries)
	}}}

	result, err := materializer.Materialize(t.Context(), runtime.CheckoutRequest{
		RepositoryFullName: "pingcap/agentteam",
		CommitSHA:          "0123456789abcdef0123456789abcdef01234567",
		Destination:        destination,
		Clean:              true,
		MaxBytes:           1024,
		MaxFiles:           10,
	})

	require.NoError(t, err)
	require.Equal(t, len(entries), result.Files)
	require.EqualValues(t, automationCheckoutTestArchiveBytes(entries), result.Bytes)
	require.FileExists(t, filepath.Join(destination, "go.mod"))
	require.FileExists(t, filepath.Join(destination, "cmd/app/main.go"))
	info, err := os.Stat(filepath.Join(destination, "bin/tool"))
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o755), info.Mode().Perm())
	linkTarget, err := os.Readlink(filepath.Join(destination, "docs/readme-link"))
	require.NoError(t, err)
	require.Equal(t, "../README.md", linkTarget)
}

func TestAGSAutomationCheckoutMaterializerRejectsUnsafeArchives(t *testing.T) {
	tests := []struct {
		name      string
		entries   []automationCheckoutTestEntry
		maxBytes  int64
		wantError string
	}{
		{
			name:      "path traversal",
			entries:   []automationCheckoutTestEntry{{name: "../evil", body: "nope"}},
			maxBytes:  1024,
			wantError: "unsafe path",
		},
		{
			name:      "symlink escape",
			entries:   []automationCheckoutTestEntry{{name: "link", link: "/tmp/outside"}},
			maxBytes:  1024,
			wantError: "unsafe symlink",
		},
		{
			name:      "byte limit",
			entries:   []automationCheckoutTestEntry{{name: "large.txt", body: "1234567890"}},
			maxBytes:  5,
			wantError: "exceeds 5 bytes",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			materializer := AGSAutomationCheckoutMaterializer{Archiver: fakeAutomationRepositoryArchiver{write: func(w io.Writer) error {
				return writeAutomationCheckoutTestArchive(w, tt.entries)
			}}}

			_, err := materializer.Materialize(t.Context(), runtime.CheckoutRequest{
				RepositoryFullName: "pingcap/agentteam",
				CommitSHA:          "0123456789abcdef0123456789abcdef01234567",
				Destination:        automationCheckoutTempDir(t),
				Clean:              true,
				MaxBytes:           tt.maxBytes,
				MaxFiles:           10,
			})

			require.Error(t, err)
			require.Contains(t, err.Error(), tt.wantError)
		})
	}
}

func TestAGSAutomationCheckoutMaterializerRejectsDestinationSymlinkAncestor(t *testing.T) {
	workspace := automationCheckoutTempDir(t)
	outside := automationCheckoutTempDir(t)
	outsideCheckout := filepath.Join(outside, "checkout")
	require.NoError(t, os.MkdirAll(outsideCheckout, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(outsideCheckout, "keep.txt"), []byte("keep"), 0o644))
	require.NoError(t, os.Symlink(outside, filepath.Join(workspace, "link")))
	materializer := AGSAutomationCheckoutMaterializer{Archiver: fakeAutomationRepositoryArchiver{write: func(w io.Writer) error {
		return writeAutomationCheckoutTestArchive(w, []automationCheckoutTestEntry{{name: "go.mod", body: "module escape.test\n"}})
	}}}

	_, err := materializer.Materialize(t.Context(), runtime.CheckoutRequest{
		RepositoryFullName: "pingcap/agentteam",
		CommitSHA:          "0123456789abcdef0123456789abcdef01234567",
		Destination:        filepath.Join(workspace, "link", "checkout"),
		Clean:              true,
		MaxBytes:           1024,
		MaxFiles:           10,
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "symlink")
	require.FileExists(t, filepath.Join(outsideCheckout, "keep.txt"))
	require.NoFileExists(t, filepath.Join(outsideCheckout, "go.mod"))
}

func automationCheckoutTempDir(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	resolved, err := filepath.EvalSymlinks(dir)
	require.NoError(t, err)
	return resolved
}

type fakeAutomationRepositoryArchiver struct {
	write func(io.Writer) error
}

func (a fakeAutomationRepositoryArchiver) ArchiveRepository(ctx context.Context, request AutomationRepositoryArchiveRequest) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	return a.write(request.Writer)
}

type automationCheckoutTestEntry struct {
	name string
	body string
	link string
	mode int64
}

func writeAutomationCheckoutTestArchive(w io.Writer, entries []automationCheckoutTestEntry) error {
	gzipWriter := gzip.NewWriter(w)
	tarWriter := tar.NewWriter(gzipWriter)
	if err := tarWriter.WriteHeader(&tar.Header{Name: "pax_global_header", Typeflag: tar.TypeXGlobalHeader}); err != nil {
		return err
	}
	for _, entry := range entries {
		mode := entry.mode
		if mode == 0 {
			mode = 0o644
		}
		header := &tar.Header{Name: entry.name, Mode: mode}
		if entry.link != "" {
			header.Typeflag = tar.TypeSymlink
			header.Linkname = entry.link
		} else {
			header.Typeflag = tar.TypeReg
			header.Size = int64(len(entry.body))
		}
		if err := tarWriter.WriteHeader(header); err != nil {
			return err
		}
		if entry.link == "" {
			if _, err := io.WriteString(tarWriter, entry.body); err != nil {
				return err
			}
		}
	}
	if err := tarWriter.Close(); err != nil {
		return err
	}
	return gzipWriter.Close()
}

func automationCheckoutTestArchiveBytes(entries []automationCheckoutTestEntry) int64 {
	var bytes int64
	for _, entry := range entries {
		if entry.link == "" {
			bytes += int64(len(entry.body))
		}
	}
	return bytes
}
