package storage

import (
	"encoding/xml"
	"strings"
	"testing"
)

func TestFolderID_Deterministic(t *testing.T) {
	a := FolderID("team-alpha", "fp1")
	b := FolderID("team-alpha", "fp1")
	if a != b {
		t.Errorf("FolderID not deterministic: %q vs %q", a, b)
	}
}

func TestFolderID_DifferentNamespaces(t *testing.T) {
	a := FolderID("team-alpha", "fp1")
	b := FolderID("team-beta", "fp1")
	if a == b {
		t.Errorf("different namespaces produced same folder ID: %q", a)
	}
}

func TestFolderID_DifferentMasterKeys(t *testing.T) {
	a := FolderID("team-alpha", "fp1")
	b := FolderID("team-alpha", "fp2")
	if a == b {
		t.Errorf("different master keys produced same folder ID: %q", a)
	}
}

func TestFolderID_EmptyFingerprintUsesPlaceholder(t *testing.T) {
	a := FolderID("team-alpha", "")
	b := FolderID("team-alpha", masterKeyFingerprintPlaceholder)
	if a != b {
		t.Errorf("empty fingerprint did not match placeholder: %q vs %q", a, b)
	}
}

func TestFolderID_Length(t *testing.T) {
	id := FolderID("ns", "fp")
	if len(id) != 32 {
		t.Errorf("folder ID length = %d, want 32", len(id))
	}
}

func TestRenderSyncthingConfig_OK(t *testing.T) {
	rep := VolumeReplication{
		Namespace:   "team-alpha",
		VolumeName:  "data",
		SourcePath:  "/var/lib/orca/volumes/data",
		ReplicateTo: []string{"peer-b", "peer-c"},
		SyncMode:    "sendreceive",
	}
	peers := []SyncthingDevice{
		{ID: "dev-source", Name: "peer-a", Address: "dynamic"},
		{ID: "dev-b", Name: "peer-b", Address: "tcp://peer-b:22000"},
		{ID: "dev-c", Name: "peer-c", Address: "tcp://peer-c:22000"},
	}
	cfg, err := RenderSyncthingConfig(rep, peers)
	if err != nil {
		t.Fatalf("RenderSyncthingConfig: %v", err)
	}
	if cfg.FolderID != FolderID("team-alpha", "") {
		t.Errorf("folder ID = %q, want %q", cfg.FolderID, FolderID("team-alpha", ""))
	}
	if cfg.Path != rep.SourcePath {
		t.Errorf("path = %q, want %q", cfg.Path, rep.SourcePath)
	}
	if len(cfg.Devices) != 3 {
		t.Errorf("devices = %d, want 3", len(cfg.Devices))
	}
	if cfg.Devices[0].ID != "dev-source" {
		t.Errorf("first device = %q, want dev-source", cfg.Devices[0].ID)
	}
}

func TestRenderSyncthingConfig_Errors(t *testing.T) {
	peers := []SyncthingDevice{{ID: "d", Name: "n", Address: "dynamic"}}
	if _, err := RenderSyncthingConfig(VolumeReplication{}, peers); err == nil {
		t.Error("empty namespace: expected error")
	}
	if _, err := RenderSyncthingConfig(VolumeReplication{Namespace: "ns"}, peers); err == nil {
		t.Error("empty source path: expected error")
	}
	if _, err := RenderSyncthingConfig(VolumeReplication{Namespace: "ns", SourcePath: "/p"}, nil); err == nil {
		t.Error("no peers: expected error")
	}
}

func TestRenderSyncthingXML_Valid(t *testing.T) {
	cfg := &SyncthingConfig{
		FolderID: "abcd1234abcd1234abcd1234abcd1234",
		Path:     "/var/lib/orca/data",
		Devices: []SyncthingDevice{
			{ID: "dev-source", Name: "peer-a", Address: "dynamic"},
			{ID: "dev-b", Name: "peer-b", Address: "tcp://peer-b:22000"},
		},
	}
	out, err := RenderSyncthingXML(cfg)
	if err != nil {
		t.Fatalf("RenderSyncthingXML: %v", err)
	}
	if !strings.HasPrefix(out, xml.Header) {
		t.Errorf("output missing XML header")
	}
	if !strings.Contains(out, `id="abcd1234abcd1234abcd1234abcd1234"`) {
		t.Errorf("folder ID not in output")
	}
	if !strings.Contains(out, "dev-source") {
		t.Errorf("source device ID not in output")
	}
	if !strings.Contains(out, "dev-b") {
		t.Errorf("peer device ID not in output")
	}
	if !strings.Contains(out, "/var/lib/orca/data") {
		t.Errorf("path not in output")
	}
	if !strings.Contains(out, `tcp://peer-b:22000`) {
		t.Errorf("peer address not in output")
	}
	if !strings.Contains(out, `<gui enabled="false"`) {
		t.Errorf("GUI not disabled in output")
	}
	// Must be parseable as XML.
	var doc syncthingXMLConfig
	if err := xml.Unmarshal([]byte(out), &doc); err != nil {
		t.Fatalf("output is not valid XML: %v", err)
	}
	if doc.Folders[0].ID != cfg.FolderID {
		t.Errorf("parsed folder ID = %q, want %q", doc.Folders[0].ID, cfg.FolderID)
	}
}

func TestRenderSyncthingXML_Deterministic(t *testing.T) {
	cfg := &SyncthingConfig{
		FolderID: "abcd1234abcd1234abcd1234abcd1234",
		Path:     "/data",
		Devices: []SyncthingDevice{
			{ID: "dev-a", Name: "a", Address: "dynamic"},
			{ID: "dev-b", Name: "b", Address: "tcp://b:22000"},
		},
	}
	a, _ := RenderSyncthingXML(cfg)
	b, _ := RenderSyncthingXML(cfg)
	if a != b {
		t.Errorf("RenderSyncthingXML not deterministic")
	}
}

func TestRenderSyncthingXML_Errors(t *testing.T) {
	if _, err := RenderSyncthingXML(nil); err == nil {
		t.Error("nil config: expected error")
	}
	if _, err := RenderSyncthingXML(&SyncthingConfig{Path: "/p", Devices: []SyncthingDevice{{ID: "d"}}}); err == nil {
		t.Error("empty folder ID: expected error")
	}
	if _, err := RenderSyncthingXML(&SyncthingConfig{FolderID: "f", Path: "/p"}); err == nil {
		t.Error("no devices: expected error")
	}
}

func TestDetectConflicts_Differ(t *testing.T) {
	peerFiles := map[string]map[string][]byte{
		"peer-a": {"f": []byte("a"), "shared": []byte("same")},
		"peer-b": {"f": []byte("b"), "shared": []byte("same")},
	}
	conflicts, err := DetectConflicts("ns", peerFiles)
	if err != nil {
		t.Fatalf("DetectConflicts: %v", err)
	}
	if len(conflicts) != 1 {
		t.Fatalf("expected 1 conflict, got %d", len(conflicts))
	}
	if conflicts[0].Path != "f" {
		t.Errorf("conflict path = %q, want f", conflicts[0].Path)
	}
}

func TestDetectConflicts_AllAgree(t *testing.T) {
	peerFiles := map[string]map[string][]byte{
		"peer-a": {"f": []byte("same"), "g": []byte("x")},
		"peer-b": {"f": []byte("same"), "g": []byte("x")},
	}
	conflicts, err := DetectConflicts("ns", peerFiles)
	if err != nil {
		t.Fatalf("DetectConflicts: %v", err)
	}
	if len(conflicts) != 0 {
		t.Errorf("expected 0 conflicts, got %d", len(conflicts))
	}
}

func TestDetectConflicts_SinglePeerNoConflict(t *testing.T) {
	peerFiles := map[string]map[string][]byte{
		"peer-a": {"f": []byte("a")},
	}
	conflicts, err := DetectConflicts("ns", peerFiles)
	if err != nil {
		t.Fatalf("DetectConflicts: %v", err)
	}
	if len(conflicts) != 0 {
		t.Errorf("single peer should not produce conflict, got %d", len(conflicts))
	}
}

func TestDetectConflicts_SortedOutput(t *testing.T) {
	peerFiles := map[string]map[string][]byte{
		"peer-a": {"z": []byte("a"), "a": []byte("a"), "m": []byte("a")},
		"peer-b": {"z": []byte("b"), "a": []byte("b"), "m": []byte("b")},
	}
	conflicts, _ := DetectConflicts("ns", peerFiles)
	if len(conflicts) != 3 {
		t.Fatalf("expected 3 conflicts, got %d", len(conflicts))
	}
	if conflicts[0].Path != "a" || conflicts[1].Path != "m" || conflicts[2].Path != "z" {
		t.Errorf("conflicts not sorted: %v", []string{conflicts[0].Path, conflicts[1].Path, conflicts[2].Path})
	}
}

func TestDetectConflicts_EmptyInput(t *testing.T) {
	conflicts, err := DetectConflicts("ns", nil)
	if err != nil {
		t.Fatalf("DetectConflicts: %v", err)
	}
	if conflicts != nil {
		t.Errorf("empty input should return nil, got %v", conflicts)
	}
}

func TestResolveConflict_SourceWins(t *testing.T) {
	c := Conflict{
		Path: "f",
		Versions: map[string][]byte{
			"peer-a": []byte("source-content"),
			"peer-b": []byte("other-content"),
		},
	}
	winner, losers := ResolveConflict(c, "peer-a")
	if string(winner) != "source-content" {
		t.Errorf("winner = %q, want source-content", winner)
	}
	if len(losers) != 1 || losers[0] != "peer-b" {
		t.Errorf("losers = %v, want [peer-b]", losers)
	}
}

func TestResolveConflict_PeersAgreeNotLosers(t *testing.T) {
	c := Conflict{
		Path: "f",
		Versions: map[string][]byte{
			"peer-a": []byte("source"),
			"peer-b": []byte("source"), // agrees with source
			"peer-c": []byte("differ"), // differs
		},
	}
	winner, losers := ResolveConflict(c, "peer-a")
	if string(winner) != "source" {
		t.Errorf("winner = %q, want source", winner)
	}
	if len(losers) != 1 || losers[0] != "peer-c" {
		t.Errorf("losers = %v, want [peer-c]", losers)
	}
}

func TestResolveConflict_Deterministic(t *testing.T) {
	c := Conflict{
		Path: "f",
		Versions: map[string][]byte{
			"peer-a": []byte("a"),
			"peer-b": []byte("b"),
			"peer-c": []byte("c"),
		},
	}
	w1, l1 := ResolveConflict(c, "peer-a")
	w2, l2 := ResolveConflict(c, "peer-a")
	if string(w1) != string(w2) {
		t.Errorf("non-deterministic winner")
	}
	if !equalStringSlices(l1, l2) {
		t.Errorf("non-deterministic losers: %v vs %v", l1, l2)
	}
}

func TestResolveConflict_UnknownSource(t *testing.T) {
	c := Conflict{
		Path: "f",
		Versions: map[string][]byte{
			"peer-a": []byte("a"),
			"peer-b": []byte("b"),
		},
	}
	winner, losers := ResolveConflict(c, "peer-z")
	if winner != nil {
		t.Errorf("unknown source winner should be nil, got %q", winner)
	}
	if losers != nil {
		t.Errorf("unknown source losers should be nil, got %v", losers)
	}
}
