package support

import (
	"os"
	"path/filepath"
	"sync"
	"testing"
)

// statePath returns a path inside a fresh temp directory for a store file.
func statePath(t *testing.T) string {
	t.Helper()
	return filepath.Join(t.TempDir(), "support.json")
}

func TestOpenMissingReturnsEmpty(t *testing.T) {
	s, err := Open(statePath(t))
	if err != nil {
		t.Fatalf("Open missing: %v", err)
	}
	if _, ok := s.Get(42); ok {
		t.Error("Get on an empty store returned a record")
	}
}

func TestOpenCorruptReturnsError(t *testing.T) {
	path := statePath(t)
	if err := os.WriteFile(path, []byte("{not json"), 0o600); err != nil {
		t.Fatalf("write corrupt: %v", err)
	}
	if _, err := Open(path); err == nil {
		t.Fatal("Open on a corrupt file: expected an error, got nil")
	}
}

func TestSetTopicPersistsAndReloads(t *testing.T) {
	path := statePath(t)
	s := New(path)
	if err := s.SetTopic(7, 100, 101, "Ann", "Lee", "annlee"); err != nil {
		t.Fatalf("SetTopic: %v", err)
	}

	// In-memory view.
	got, ok := s.Get(7)
	if !ok || got.TopicID != 100 || got.HeaderMsgID != 101 || got.Username != "annlee" {
		t.Fatalf("Get = %+v ok=%v, want topic 100 / header 101 / annlee", got, ok)
	}
	if got.CreatedAt.IsZero() {
		t.Error("CreatedAt not set on first contact")
	}
	if uid, ok := s.ByTopic(100); !ok || uid != 7 {
		t.Errorf("ByTopic(100) = %d ok=%v, want 7/true", uid, ok)
	}

	// Reloaded from disk.
	reload, err := Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	got, ok = reload.Get(7)
	if !ok || got.TopicID != 100 || got.FirstName != "Ann" {
		t.Fatalf("reloaded Get = %+v ok=%v, want topic 100 / Ann", got, ok)
	}
	if uid, ok := reload.ByTopic(100); !ok || uid != 7 {
		t.Errorf("reloaded ByTopic(100) = %d ok=%v, want 7/true", uid, ok)
	}
}

func TestSetTopicRecreateReindexes(t *testing.T) {
	s := New(statePath(t))
	if err := s.SetTopic(7, 100, 101, "Ann", "", ""); err != nil {
		t.Fatalf("SetTopic: %v", err)
	}
	if err := s.SetTopic(7, 200, 201, "Ann", "", ""); err != nil {
		t.Fatalf("SetTopic recreate: %v", err)
	}
	if _, ok := s.ByTopic(100); ok {
		t.Error("ByTopic(100) still resolves after recreate; the stale topic was not dropped")
	}
	if uid, ok := s.ByTopic(200); !ok || uid != 7 {
		t.Errorf("ByTopic(200) = %d ok=%v, want 7/true", uid, ok)
	}
}

// TestBlockSurvivesTopicRecreate is the field-isolation guarantee: a topic recreate
// (SetTopic) must not clear a block set concurrently from the buttons.
func TestBlockSurvivesTopicRecreate(t *testing.T) {
	s := New(statePath(t))
	if err := s.SetTopic(7, 100, 101, "Ann", "", "annlee"); err != nil {
		t.Fatalf("SetTopic: %v", err)
	}
	if err := s.SetBlocked(7, true); err != nil {
		t.Fatalf("SetBlocked: %v", err)
	}
	if err := s.SetTopic(7, 200, 201, "Ann", "", "annlee"); err != nil {
		t.Fatalf("SetTopic recreate: %v", err)
	}
	if !s.Blocked(7) {
		t.Error("block was cleared by a topic recreate")
	}
}

func TestAppendAndClearMsgs(t *testing.T) {
	s := New(statePath(t))
	if err := s.SetTopic(7, 100, 101, "Ann", "", ""); err != nil {
		t.Fatalf("SetTopic: %v", err)
	}
	for _, id := range []int{500, 501, 502} {
		if err := s.AppendMsg(7, id); err != nil {
			t.Fatalf("AppendMsg %d: %v", id, err)
		}
	}
	got, _ := s.Get(7)
	if len(got.RelayedMsgIDs) != 3 {
		t.Fatalf("RelayedMsgIDs = %v, want 3 ids", got.RelayedMsgIDs)
	}

	ids, err := s.ClearMsgs(7)
	if err != nil {
		t.Fatalf("ClearMsgs: %v", err)
	}
	if len(ids) != 3 || ids[0] != 500 || ids[2] != 502 {
		t.Errorf("ClearMsgs = %v, want [500 501 502]", ids)
	}
	got, _ = s.Get(7)
	if len(got.RelayedMsgIDs) != 0 {
		t.Errorf("RelayedMsgIDs after clear = %v, want empty", got.RelayedMsgIDs)
	}
	if got.HeaderMsgID != 101 || got.TopicID != 100 {
		t.Errorf("clear disturbed the topic/header: %+v", got)
	}

	// A second clear is a no-op.
	if ids, err := s.ClearMsgs(7); err != nil || ids != nil {
		t.Errorf("second ClearMsgs = %v, %v, want nil, nil", ids, err)
	}
}

func TestGetReturnsDetachedCopy(t *testing.T) {
	s := New(statePath(t))
	if err := s.SetTopic(7, 100, 101, "Ann", "", ""); err != nil {
		t.Fatalf("SetTopic: %v", err)
	}
	if err := s.AppendMsg(7, 500); err != nil {
		t.Fatalf("AppendMsg: %v", err)
	}
	got, _ := s.Get(7)
	got.RelayedMsgIDs[0] = 999 // mutate the copy
	again, _ := s.Get(7)
	if again.RelayedMsgIDs[0] != 500 {
		t.Error("Get returned a slice that aliases the store's state")
	}
}

// TestConcurrentMutationsSafe exercises the mutex under parallel writers; run with
// -race to catch data races.
func TestConcurrentMutationsSafe(t *testing.T) {
	s := New(statePath(t))
	if err := s.SetTopic(7, 100, 101, "Ann", "", ""); err != nil {
		t.Fatalf("SetTopic: %v", err)
	}
	var wg sync.WaitGroup
	for i := range 20 {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			_ = s.AppendMsg(7, 1000+n)
			_ = s.SetBlocked(7, n%2 == 0)
			_, _ = s.Get(7)
			_, _ = s.ByTopic(100)
		}(i)
	}
	wg.Wait()
	got, ok := s.Get(7)
	if !ok || len(got.RelayedMsgIDs) != 20 {
		t.Errorf("after concurrent appends: %d ids, want 20", len(got.RelayedMsgIDs))
	}
}
