package gitx

import (
	"strings"
	"testing"

	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/filemode"
	"github.com/go-git/go-git/v5/plumbing/format/diff"
)

// --- fakes implementing the go-git diff interfaces for pure-function tests ---

type fakeFile struct{ path string }

func (f fakeFile) Hash() plumbing.Hash     { return plumbing.ZeroHash }
func (f fakeFile) Mode() filemode.FileMode { return filemode.Regular }
func (f fakeFile) Path() string            { return f.path }

type fakeChunk struct {
	content string
	op      diff.Operation
}

func (c fakeChunk) Content() string      { return c.content }
func (c fakeChunk) Type() diff.Operation { return c.op }

type fakeFilePatch struct {
	from, to diff.File
	binary   bool
	chunks   []diff.Chunk
}

func (f fakeFilePatch) IsBinary() bool                { return f.binary }
func (f fakeFilePatch) Files() (diff.File, diff.File) { return f.from, f.to }
func (f fakeFilePatch) Chunks() []diff.Chunk          { return f.chunks }

func file(p string) diff.File { return fakeFile{path: p} }

func TestMapFilePatches(t *testing.T) {
	add := func(s string) diff.Chunk { return fakeChunk{content: s, op: diff.Add} }
	del := func(s string) diff.Chunk { return fakeChunk{content: s, op: diff.Delete} }
	eq := func(s string) diff.Chunk { return fakeChunk{content: s, op: diff.Equal} }

	tests := []struct {
		name string
		fp   fakeFilePatch
		want FileChange
	}{
		{
			"added",
			fakeFilePatch{to: file("new.txt"), chunks: []diff.Chunk{add("a\nb\n")}},
			FileChange{Status: "A", Path: "new.txt", Additions: 2},
		},
		{
			"deleted",
			fakeFilePatch{from: file("gone.txt"), chunks: []diff.Chunk{del("x\n")}},
			FileChange{Status: "D", Path: "gone.txt", Deletions: 1},
		},
		{
			"modified",
			fakeFilePatch{
				from:   file("a.txt"),
				to:     file("a.txt"),
				chunks: []diff.Chunk{eq("ctx\n"), add("n\n"), del("o1\no2\n")},
			},
			FileChange{Status: "M", Path: "a.txt", Additions: 1, Deletions: 2},
		},
		{
			"renamed with edit",
			fakeFilePatch{
				from:   file("old.txt"),
				to:     file("new.txt"),
				chunks: []diff.Chunk{add("x\n"), del("y\n")},
			},
			FileChange{Status: "R", OldPath: "old.txt", Path: "new.txt", Additions: 1, Deletions: 1},
		},
		{
			"binary skips counts",
			fakeFilePatch{
				from:   file("img.bin"),
				to:     file("img.bin"),
				binary: true,
				chunks: []diff.Chunk{add("should be ignored\n")},
			},
			FileChange{Status: "M", Path: "img.bin", Binary: true},
		},
		{
			"unterminated final line counts as one",
			fakeFilePatch{to: file("f"), chunks: []diff.Chunk{add("noeol")}},
			FileChange{Status: "A", Path: "f", Additions: 1},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := mapFilePatches([]diff.FilePatch{tt.fp})
			if len(got) != 1 {
				t.Fatalf("len = %d, want 1", len(got))
			}
			if got[0] != tt.want {
				t.Errorf("got %+v, want %+v", got[0], tt.want)
			}
		})
	}

	t.Run("empty from and to is skipped", func(t *testing.T) {
		got := mapFilePatches([]diff.FilePatch{fakeFilePatch{}})
		if len(got) != 0 {
			t.Errorf("got %+v, want none", got)
		}
	})
}

func TestCutPatch(t *testing.T) {
	fileA := "diff --git a/a.txt b/a.txt\n@@ -1 +1 @@\n-x\n+y\n"
	fileB := "diff --git a/b.txt b/b.txt\n@@ -1 +1 @@\n-p\n+q\n"
	full := fileA + fileB

	t.Run("under limit unchanged", func(t *testing.T) {
		got, trunc := cutPatch(full, 1<<20)
		if trunc || got != full {
			t.Errorf("trunc=%v got=%q", trunc, got)
		}
	})

	t.Run("zero limit unchanged", func(t *testing.T) {
		got, trunc := cutPatch(full, 0)
		if trunc || got != full {
			t.Errorf("trunc=%v", trunc)
		}
	})

	t.Run("cut at file boundary drops torn file", func(t *testing.T) {
		// A limit that lands inside fileB must cut back to the end of fileA.
		limit := int64(len(fileA) + 10)
		got, trunc := cutPatch(full, limit)
		if !trunc {
			t.Fatalf("expected truncated")
		}
		if got != fileA {
			t.Errorf("got %q, want just fileA (no torn diff --git)", got)
		}
		if strings.Count(got, "diff --git") != 1 {
			t.Errorf("torn file leaked: %q", got)
		}
	})

	t.Run("single huge leading file cut hard at limit", func(t *testing.T) {
		huge := "diff --git a/big b/big\n" + strings.Repeat("+line\n", 1000)
		got, trunc := cutPatch(huge, 40)
		if !trunc {
			t.Fatalf("expected truncated")
		}
		if int64(len(got)) != 40 {
			t.Errorf("len = %d, want 40", len(got))
		}
	})
}
