package storage_test

import (
	"errors"
	"strings"
	"testing"

	"github.com/uopi/uopi/internal/storage"
)

// mkVersioned creates an interface and pushes it to version 3 by updating
// twice, returning the id. After this the store holds: current (v3) plus
// backups id.v1.xml and id.v2.xml.
func mkVersioned(t *testing.T) (*storage.Store, string) {
	t.Helper()
	s := newStore(t)
	id, err := s.Create([]byte(`<interface id="vers" name="V1" version="1"><widget/></interface>`), "")
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	if err := s.Update(id, []byte(`<interface id="vers" name="V2" version="1"><widget/></interface>`), "second"); err != nil {
		t.Fatalf("Update→v2: %v", err)
	}
	if err := s.Update(id, []byte(`<interface id="vers" name="V3" version="1"><widget/></interface>`), ""); err != nil {
		t.Fatalf("Update→v3: %v", err)
	}
	return s, id
}

// -------------------------------------------------------------------------- //
// Groups                                                                       //
// -------------------------------------------------------------------------- //

func TestReadGroupsDefaultsEmptyArray(t *testing.T) {
	s := newStore(t)
	data, err := s.ReadGroups()
	if err != nil {
		t.Fatalf("ReadGroups: %v", err)
	}
	if string(data) != "[]" {
		t.Errorf("ReadGroups default = %q, want %q", data, "[]")
	}
}

func TestWriteThenReadGroups(t *testing.T) {
	s := newStore(t)
	want := `[{"id":"a","name":"Area A"}]`
	if err := s.WriteGroups([]byte(want)); err != nil {
		t.Fatalf("WriteGroups: %v", err)
	}
	got, err := s.ReadGroups()
	if err != nil {
		t.Fatalf("ReadGroups: %v", err)
	}
	if string(got) != want {
		t.Errorf("ReadGroups = %q, want %q", got, want)
	}
}

// -------------------------------------------------------------------------- //
// Versions                                                                     //
// -------------------------------------------------------------------------- //

func TestVersionsNewestFirstWithCurrentFlag(t *testing.T) {
	s, id := mkVersioned(t)
	vs, err := s.Versions(id)
	if err != nil {
		t.Fatalf("Versions: %v", err)
	}
	if len(vs) != 3 {
		t.Fatalf("expected 3 versions, got %d", len(vs))
	}
	if vs[0].Version != 3 || vs[1].Version != 2 || vs[2].Version != 1 {
		t.Errorf("not newest-first: %d, %d, %d", vs[0].Version, vs[1].Version, vs[2].Version)
	}
	if !vs[0].Current {
		t.Error("v3 should be flagged Current")
	}
	if vs[1].Current || vs[2].Current {
		t.Error("backup revisions must not be flagged Current")
	}
	// The "second" tag was stamped on the revision that became backup v2.
	if vs[1].Tag != "second" {
		t.Errorf("v2 tag = %q, want %q", vs[1].Tag, "second")
	}
}

func TestVersionsNotFound(t *testing.T) {
	s := newStore(t)
	if _, err := s.Versions("ghost"); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("Versions missing: want ErrNotFound, got %v", err)
	}
}

func TestVersionsInvalidID(t *testing.T) {
	s := newStore(t)
	if _, err := s.Versions("../x"); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("Versions bad ID: want ErrNotFound, got %v", err)
	}
}

// -------------------------------------------------------------------------- //
// GetVersion                                                                   //
// -------------------------------------------------------------------------- //

func TestGetVersionCurrentAndBackup(t *testing.T) {
	s, id := mkVersioned(t)

	cur, err := s.GetVersion(id, 3)
	if err != nil {
		t.Fatalf("GetVersion(3): %v", err)
	}
	if !strings.Contains(string(cur), `name="V3"`) {
		t.Errorf("GetVersion(3) wrong content: %s", cur)
	}

	old, err := s.GetVersion(id, 1)
	if err != nil {
		t.Fatalf("GetVersion(1): %v", err)
	}
	if !strings.Contains(string(old), `name="V1"`) {
		t.Errorf("GetVersion(1) wrong content: %s", old)
	}
}

func TestGetVersionMissingRevision(t *testing.T) {
	s, id := mkVersioned(t)
	if _, err := s.GetVersion(id, 99); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("GetVersion(99): want ErrNotFound, got %v", err)
	}
}

func TestGetVersionMissingInterface(t *testing.T) {
	s := newStore(t)
	if _, err := s.GetVersion("ghost", 1); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("GetVersion missing iface: want ErrNotFound, got %v", err)
	}
}

// -------------------------------------------------------------------------- //
// SetVersionTag                                                                //
// -------------------------------------------------------------------------- //

func TestSetVersionTagOnBackupAndClear(t *testing.T) {
	s, id := mkVersioned(t)

	// Tag a backup revision in place (no new revision created).
	if err := s.SetVersionTag(id, 1, "milestone"); err != nil {
		t.Fatalf("SetVersionTag(1): %v", err)
	}
	vs, _ := s.Versions(id)
	if len(vs) != 3 {
		t.Fatalf("tagging must not add a revision: got %d", len(vs))
	}
	var v1 *storage.VersionMeta
	for i := range vs {
		if vs[i].Version == 1 {
			v1 = &vs[i]
		}
	}
	if v1 == nil || v1.Tag != "milestone" {
		t.Fatalf("v1 tag not set: %+v", v1)
	}

	// Clearing the tag.
	if err := s.SetVersionTag(id, 1, ""); err != nil {
		t.Fatalf("SetVersionTag clear: %v", err)
	}
	vs, _ = s.Versions(id)
	for _, v := range vs {
		if v.Version == 1 && v.Tag != "" {
			t.Errorf("v1 tag should be cleared, got %q", v.Tag)
		}
	}
}

func TestSetVersionTagCurrent(t *testing.T) {
	s, id := mkVersioned(t)
	if err := s.SetVersionTag(id, 3, "live"); err != nil {
		t.Fatalf("SetVersionTag(3): %v", err)
	}
	vs, _ := s.Versions(id)
	if vs[0].Version != 3 || vs[0].Tag != "live" {
		t.Errorf("current tag not applied: %+v", vs[0])
	}
}

func TestSetVersionTagMissing(t *testing.T) {
	s, id := mkVersioned(t)
	if err := s.SetVersionTag(id, 99, "x"); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("SetVersionTag missing rev: want ErrNotFound, got %v", err)
	}
	if err := s.SetVersionTag("../bad", 1, "x"); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("SetVersionTag bad ID: want ErrNotFound, got %v", err)
	}
}

// -------------------------------------------------------------------------- //
// Promote                                                                      //
// -------------------------------------------------------------------------- //

func TestPromoteIsNonDestructive(t *testing.T) {
	s, id := mkVersioned(t)

	// Promote v1 — its content becomes the new current revision (v4), while
	// every prior revision is preserved.
	if err := s.Promote(id, 1); err != nil {
		t.Fatalf("Promote(1): %v", err)
	}
	vs, err := s.Versions(id)
	if err != nil {
		t.Fatalf("Versions after promote: %v", err)
	}
	if len(vs) != 4 {
		t.Fatalf("expected 4 versions after promote, got %d", len(vs))
	}
	if vs[0].Version != 4 || !vs[0].Current {
		t.Errorf("new current should be v4: %+v", vs[0])
	}
	if vs[0].Name != "V1" {
		t.Errorf("promoted content should be V1's, got name %q", vs[0].Name)
	}
	if vs[0].Tag != "restored from v1" {
		t.Errorf("promote tag = %q", vs[0].Tag)
	}
}

func TestPromoteMissing(t *testing.T) {
	s, id := mkVersioned(t)
	if err := s.Promote(id, 99); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("Promote missing rev: want ErrNotFound, got %v", err)
	}
}

// -------------------------------------------------------------------------- //
// Fork                                                                         //
// -------------------------------------------------------------------------- //

func TestForkResetsVersionAndClearsTag(t *testing.T) {
	s, id := mkVersioned(t)
	if err := s.SetVersionTag(id, 2, "tagged"); err != nil {
		t.Fatalf("SetVersionTag: %v", err)
	}

	newID, err := s.Fork(id, 2)
	if err != nil {
		t.Fatalf("Fork(2): %v", err)
	}
	if newID == id {
		t.Fatal("Fork must produce a distinct ID")
	}

	data, err := s.Get(newID)
	if err != nil {
		t.Fatalf("Get(fork): %v", err)
	}
	str := string(data)
	if !strings.Contains(str, `name="V2"`) {
		t.Errorf("fork should carry V2 content: %s", str)
	}
	if !strings.Contains(str, `version="1"`) {
		t.Errorf("fork version should reset to 1: %s", str)
	}
	if !strings.Contains(str, `id="`+newID+`"`) {
		t.Errorf("fork id attr should be stamped: %s", str)
	}
	if strings.Contains(str, "tagged") {
		t.Errorf("fork should clear the tag: %s", str)
	}

	// The fork is an independent, listable interface at v1.
	vs, err := s.Versions(newID)
	if err != nil {
		t.Fatalf("Versions(fork): %v", err)
	}
	if len(vs) != 1 || vs[0].Version != 1 {
		t.Errorf("fork should have a single v1 revision, got %+v", vs)
	}
}

func TestForkMissing(t *testing.T) {
	s := newStore(t)
	if _, err := s.Fork("ghost", 1); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("Fork missing: want ErrNotFound, got %v", err)
	}
}

// -------------------------------------------------------------------------- //
// Create: slug derivation + attribute stamping                                 //
// -------------------------------------------------------------------------- //

func TestCreateSlugifiesNameWhenNoID(t *testing.T) {
	s := newStore(t)
	id, err := s.Create([]byte(`<interface name="My Cool Panel!" version="1"/>`), "")
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	if id != "my-cool-panel" {
		t.Errorf("slug id = %q, want %q", id, "my-cool-panel")
	}
}

func TestCreateEmptyNameFallsBackToInterface(t *testing.T) {
	s := newStore(t)
	id, err := s.Create([]byte(`<interface name="" version="1"/>`), "")
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	if id != "interface" {
		t.Errorf("fallback id = %q, want %q", id, "interface")
	}
}

func TestCreateStampsMissingVersionAndID(t *testing.T) {
	s := newStore(t)
	// XML carries neither id nor version attributes — Create must insert both.
	id, err := s.Create([]byte(`<interface name="Stamp Me"><widget/></interface>`), "label")
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	data, err := s.Get(id)
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	str := string(data)
	if !strings.Contains(str, `version="1"`) {
		t.Errorf("Create should stamp version=1: %s", str)
	}
	if !strings.Contains(str, `id="`+id+`"`) {
		t.Errorf("Create should stamp id: %s", str)
	}
	if !strings.Contains(str, `tag="label"`) {
		t.Errorf("Create should stamp tag: %s", str)
	}
}

// -------------------------------------------------------------------------- //
// Update tagging + backup chain                                                //
// -------------------------------------------------------------------------- //

func TestDeleteMovesVersionedBackups(t *testing.T) {
	s, id := mkVersioned(t) // current + id.v1.xml + id.v2.xml
	if err := s.Delete(id); err != nil {
		t.Fatalf("Delete: %v", err)
	}
	if _, err := s.Get(id); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("Get after delete: want ErrNotFound, got %v", err)
	}
	if _, err := s.Versions(id); !errors.Is(err, storage.ErrNotFound) {
		t.Errorf("Versions after delete: want ErrNotFound, got %v", err)
	}
}

func TestListSkipsVersionedBackups(t *testing.T) {
	s, _ := mkVersioned(t) // leaves id.v1.xml and id.v2.xml on disk
	list, err := s.List()
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	if len(list) != 1 {
		t.Fatalf("List should skip versioned backups, got %d entries: %+v", len(list), list)
	}
}

func TestUpdateStampsIncrementingVersion(t *testing.T) {
	s, id := mkVersioned(t)
	data, err := s.Get(id)
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	if !strings.Contains(string(data), `version="3"`) {
		t.Errorf("current should be version 3 after two updates: %s", data)
	}
}
