package installmeta

import (
	"errors"
	"os"
	"os/user"
	"path/filepath"
	"strconv"
	"testing"
	"time"
)

func TestSaveLoadRoundTrip(t *testing.T) {
	path := filepath.Join(t.TempDir(), "install.toml")
	want := Metadata{
		OwnerUser:   "dev",
		OwnerUID:    1000,
		OwnerGID:    1000,
		OwnerHome:   "/home/dev",
		InstalledAt: time.Unix(1710000000, 0).UTC(),
		Version:     "v1.2.3",
		Commit:      "abc123",
		BuiltAt:     "2026-04-23T00:00:00Z",
	}

	if err := Save(path, want); err != nil {
		t.Fatalf("Save: %v", err)
	}
	got, err := Load(path)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if got != want {
		t.Fatalf("Load() = %+v, want %+v", got, want)
	}
}

func TestSaveCreatesParentDir(t *testing.T) {
	path := filepath.Join(t.TempDir(), "nested", "dir", "install.toml")
	meta := Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"}
	if err := Save(path, meta); err != nil {
		t.Fatalf("Save: %v", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Fatalf("file not written: %v", err)
	}
}

func TestSaveRejectsInvalidMetadata(t *testing.T) {
	path := filepath.Join(t.TempDir(), "install.toml")
	if err := Save(path, Metadata{OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"}); err == nil {
		t.Fatal("Save() = nil, want validation error")
	}
	if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("Save wrote a file despite validation error: stat err = %v", err)
	}
}

func TestLoadMissingFile(t *testing.T) {
	_, err := Load(filepath.Join(t.TempDir(), "missing.toml"))
	if !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("Load() = %v, want os.ErrNotExist", err)
	}
}

func TestLoadInvalidTOML(t *testing.T) {
	path := filepath.Join(t.TempDir(), "install.toml")
	if err := os.WriteFile(path, []byte("not = valid = toml\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := Load(path); err == nil {
		t.Fatal("Load() = nil, want TOML parse error")
	}
}

func TestLoadRejectsInvalidPersistedMetadata(t *testing.T) {
	// File parses but fails Validate (no owner_user) — Load must surface
	// the validation error rather than returning a zero-value Metadata.
	path := filepath.Join(t.TempDir(), "install.toml")
	if err := os.WriteFile(path, []byte("owner_uid = 1\nowner_gid = 1\nowner_home = \"/home/dev\"\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := Load(path); err == nil {
		t.Fatal("Load() = nil, want validation error")
	}
}

func TestValidate(t *testing.T) {
	tests := []struct {
		name string
		m    Metadata
		ok   bool
	}{
		{"valid", Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"}, true},
		{"missing owner_user", Metadata{OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"}, false},
		{"whitespace owner_user", Metadata{OwnerUser: "  ", OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"}, false},
		{"negative uid", Metadata{OwnerUser: "dev", OwnerUID: -1, OwnerGID: 1, OwnerHome: "/home/dev"}, false},
		{"negative gid", Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: -1, OwnerHome: "/home/dev"}, false},
		{"empty home", Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: 1, OwnerHome: ""}, false},
		{"relative home", Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: 1, OwnerHome: "home/dev"}, false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := tc.m.Validate()
			if tc.ok && err != nil {
				t.Fatalf("Validate() = %v, want nil", err)
			}
			if !tc.ok && err == nil {
				t.Fatal("Validate() = nil, want error")
			}
		})
	}
}

func TestLookupOwnerEmpty(t *testing.T) {
	if _, err := LookupOwner(""); err == nil {
		t.Fatal("LookupOwner(\"\") = nil, want error")
	}
	if _, err := LookupOwner("   "); err == nil {
		t.Fatal("LookupOwner(\" \") = nil, want error")
	}
}

func TestLookupOwnerMissing(t *testing.T) {
	if _, err := LookupOwner("definitely-no-such-user-banger-test"); err == nil {
		t.Fatal("LookupOwner(missing) = nil, want error")
	}
}

func TestLookupOwnerCurrentUser(t *testing.T) {
	cur, err := user.Current()
	if err != nil {
		t.Skipf("user.Current: %v", err)
	}
	got, err := LookupOwner(cur.Username)
	if err != nil {
		t.Fatalf("LookupOwner(%q): %v", cur.Username, err)
	}
	wantUID, _ := strconv.Atoi(cur.Uid)
	wantGID, _ := strconv.Atoi(cur.Gid)
	if got.OwnerUser != cur.Username || got.OwnerUID != wantUID || got.OwnerGID != wantGID || got.OwnerHome != cur.HomeDir {
		t.Fatalf("LookupOwner = %+v, want user=%s uid=%d gid=%d home=%s",
			got, cur.Username, wantUID, wantGID, cur.HomeDir)
	}
}

func TestUpdateBuildInfo(t *testing.T) {
	path := filepath.Join(t.TempDir(), "install.toml")
	original := Metadata{
		OwnerUser:   "dev",
		OwnerUID:    1000,
		OwnerGID:    1000,
		OwnerHome:   "/home/dev",
		InstalledAt: time.Unix(1710000000, 0).UTC(),
		Version:     "v0.1.0",
		Commit:      "old",
		BuiltAt:     "2026-01-01T00:00:00Z",
	}
	if err := Save(path, original); err != nil {
		t.Fatalf("Save: %v", err)
	}

	if err := UpdateBuildInfo(path, "  v0.2.0 ", " new ", " 2026-04-30T00:00:00Z "); err != nil {
		t.Fatalf("UpdateBuildInfo: %v", err)
	}

	got, err := Load(path)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if got.Version != "v0.2.0" || got.Commit != "new" || got.BuiltAt != "2026-04-30T00:00:00Z" {
		t.Fatalf("build fields = %q/%q/%q, want trimmed values", got.Version, got.Commit, got.BuiltAt)
	}
	// Identity must be preserved.
	if got.OwnerUser != original.OwnerUser || got.OwnerUID != original.OwnerUID ||
		got.OwnerGID != original.OwnerGID || got.OwnerHome != original.OwnerHome ||
		!got.InstalledAt.Equal(original.InstalledAt) {
		t.Fatalf("identity changed: got %+v, want %+v", got, original)
	}
}

func TestUpdateBuildInfoMissingFile(t *testing.T) {
	err := UpdateBuildInfo(filepath.Join(t.TempDir(), "missing.toml"), "v1", "c", "t")
	if !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("UpdateBuildInfo() = %v, want os.ErrNotExist", err)
	}
}

func TestValidateRejectsMissingOwner(t *testing.T) {
	err := Metadata{OwnerUID: 1000, OwnerGID: 1000, OwnerHome: "/home/dev"}.Validate()
	if err == nil {
		t.Fatal("Validate() = nil, want missing owner_user error")
	}
}
