package profile

import (
	"context"
	"crypto/sha256"
	"errors"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"testing"

	"github.com/pingcap/failpoint"
	"github.com/stretchr/testify/require"
	"go.yaml.in/yaml/v3"

	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

type testServerTransition struct {
	mu         sync.Mutex
	completed  int
	failClosed int
}

func (t *testServerTransition) Complete() {
	t.mu.Lock()
	t.completed++
	t.mu.Unlock()
}

func (t *testServerTransition) FailClosed() {
	t.mu.Lock()
	t.failClosed++
	t.mu.Unlock()
}

type testServerRuntimeParticipant struct {
	mu          sync.Mutex
	transition  *testServerTransition
	reconcile   func(previous, current *AgentTeamServer) error
	repair      func(current AgentTeamServer) error
	invalidated []string
}

type blockingServerTransitionParticipant struct {
	*testServerRuntimeParticipant
	entered chan string
	release map[string]chan struct{}
}

func (p *blockingServerTransitionParticipant) ReconcileServer(
	_ context.Context,
	previous, current *AgentTeamServer,
) error {
	id := ""
	if current != nil {
		id = current.ID
	} else if previous != nil {
		id = previous.ID
	}
	p.entered <- id
	<-p.release[id]
	return nil
}

func (p *testServerRuntimeParticipant) BeginServerTransition(string) ServerTransition {
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.transition == nil {
		p.transition = &testServerTransition{}
	}
	return p.transition
}

func (p *testServerRuntimeParticipant) ReconcileServer(
	_ context.Context,
	previous, current *AgentTeamServer,
) error {
	if p.reconcile != nil {
		return p.reconcile(previous, current)
	}
	return nil
}

func (p *testServerRuntimeParticipant) RepairServer(_ context.Context, current AgentTeamServer) error {
	if p.repair != nil {
		return p.repair(current)
	}
	return nil
}

func (p *testServerRuntimeParticipant) InvalidateServer(id string) {
	p.mu.Lock()
	p.invalidated = append(p.invalidated, id)
	p.mu.Unlock()
}

func activateTestServerRuntime(t *testing.T, manager *Manager, participant *testServerRuntimeParticipant) {
	t.Helper()
	require.NoError(t, manager.Servers().PrepareRuntimeMode())
	require.NoError(t, manager.Servers().ActivateRuntimeMode(participant))
	t.Cleanup(manager.Servers().DeactivateRuntimeMode)
}

func TestServerRuntimeLifecycleNeverSilentlyDowngradesPreparingMode(t *testing.T) {
	manager := New(t.TempDir())
	server := &AgentTeamServer{ID: "server-a", Enabled: true}
	require.NoError(t, manager.Servers().Add(server))
	require.NoError(t, manager.Servers().PrepareRuntimeMode())

	name := "blocked"
	_, err := manager.Servers().Patch(server.ID, ServerUpdate{Name: &name})
	require.ErrorIs(t, err, ErrServerRuntimePreparing)

	manager.Servers().DeactivateRuntimeMode()
	updated, err := manager.Servers().Patch(server.ID, ServerUpdate{Name: &name})
	require.NoError(t, err)
	require.Equal(t, name, updated.Name)
}

func TestPatchConnectionStatusMergesAgainstCurrentIdentity(t *testing.T) {
	manager := New(t.TempDir())
	server := &AgentTeamServer{
		ID:          "server-a",
		Enabled:     true,
		ServerURL:   "https://old.example.test",
		WorkspaceID: "wsp-old",
	}
	require.NoError(t, manager.Servers().Add(server))
	reconciles := 0
	participant := &testServerRuntimeParticipant{
		reconcile: func(*AgentTeamServer, *AgentTeamServer) error {
			reconciles++
			return nil
		},
	}
	activateTestServerRuntime(t, manager, participant)

	workspace := "wsp-new"
	cfg, err := yaml.Marshal(legacyAgentTeamConfig{
		ServerURL:   server.ServerURL,
		WorkspaceID: workspace,
	})
	require.NoError(t, err)
	_, err = manager.Servers().Patch(server.ID, ServerUpdate{
		Config: rawMessagePointer(cfg),
	})
	require.NoError(t, err)

	updated, err := manager.Servers().PatchConnectionStatus(server.ID, "invalid_machine_api_key")
	require.NoError(t, err)
	require.Equal(t, workspace, updated.WorkspaceID)
	require.Equal(t, "invalid_machine_api_key", updated.ConnectionStatus)

	updated, err = manager.Servers().PatchConnectionStatus(server.ID, "")
	require.NoError(t, err)
	require.Equal(t, workspace, updated.WorkspaceID)
	require.Empty(t, updated.ConnectionStatus)
	var persistedConfig legacyAgentTeamConfig
	require.NoError(t, yaml.Unmarshal(updated.Config, &persistedConfig))
	require.Empty(t, persistedConfig.ConnectionStatus)
	require.Equal(t, 1, reconciles, "workspace mutation reconciles once; status-only patch must not restart runtime")
}

func TestServerConfigPatchClearsExplicitConnectionStatus(t *testing.T) {
	manager := New(t.TempDir())
	server := &AgentTeamServer{
		ID:               "server-a",
		Enabled:          true,
		ServerURL:        "https://agentteam.example.test",
		APIKey:           "machine-key",
		WorkspaceID:      "wsp-one",
		MachineID:        "mac-one",
		ConnectionStatus: "invalid_machine_api_key",
	}
	require.NoError(t, manager.Servers().Add(server))
	reconciles := 0
	activateTestServerRuntime(t, manager, &testServerRuntimeParticipant{
		reconcile: func(*AgentTeamServer, *AgentTeamServer) error {
			reconciles++
			return nil
		},
	})

	config := types.RawMessage(`server_url: https://agentteam.example.test
api_key: machine-key
workspace_id: wsp-one
machine_id: mac-one
connection_status: ""
`)
	updated, err := manager.Servers().Patch(server.ID, ServerUpdate{Config: &config})
	require.NoError(t, err)
	require.Empty(t, updated.ConnectionStatus)
	require.Equal(t, 1, reconciles)
}

func TestRuntimeRollbackFailureFailsClosed(t *testing.T) {
	manager := New(t.TempDir())
	server := &AgentTeamServer{ID: "server-a", Enabled: true, WorkspaceID: "wsp-old"}
	require.NoError(t, manager.Servers().Add(server))
	participant := &testServerRuntimeParticipant{
		reconcile: func(*AgentTeamServer, *AgentTeamServer) error {
			return errors.New("runtime unavailable")
		},
	}
	activateTestServerRuntime(t, manager, participant)

	nextWorkspace := "wsp-new"
	cfg, err := yaml.Marshal(legacyAgentTeamConfig{WorkspaceID: nextWorkspace})
	require.NoError(t, err)
	_, err = manager.Servers().Patch(server.ID, ServerUpdate{Config: rawMessagePointer(cfg)})
	require.ErrorContains(t, err, "runtime rollback failed")
	require.Equal(t, []string{server.ID}, participant.invalidated)
	require.Zero(t, participant.transition.completed)
	require.Equal(t, 1, participant.transition.failClosed)
	require.True(t, manager.Servers().ServerRuntimeHalted(server.ID))
	require.False(t, manager.Servers().ServerAvailableForSelection(server.ID))
	name := "blocked"
	_, patchErr := manager.Servers().Patch(server.ID, ServerUpdate{Name: &name})
	require.ErrorIs(t, patchErr, ErrServerRuntimeHalted)
	current, ok := manager.Servers().Get(server.ID)
	require.True(t, ok)
	require.Equal(t, "wsp-old", current.WorkspaceID)
}

func TestRepairServerRuntimeClearsQuarantineOnlyAfterSuccess(t *testing.T) {
	manager := New(t.TempDir())
	server := &AgentTeamServer{ID: "server-a", Enabled: true, WorkspaceID: "wsp-old"}
	require.NoError(t, manager.Servers().Add(server))
	fail := true
	participant := &testServerRuntimeParticipant{
		reconcile: func(previous, _ *AgentTeamServer) error {
			if previous != nil {
				return errors.New("runtime unavailable")
			}
			return nil
		},
		repair: func(AgentTeamServer) error {
			if fail {
				return errors.New("runtime unavailable")
			}
			return nil
		},
	}
	activateTestServerRuntime(t, manager, participant)

	cfg, err := yaml.Marshal(legacyAgentTeamConfig{WorkspaceID: "wsp-new"})
	require.NoError(t, err)
	_, err = manager.Servers().Patch(server.ID, ServerUpdate{Config: rawMessagePointer(cfg)})
	require.Error(t, err)
	require.True(t, manager.Servers().ServerRuntimeHalted(server.ID))

	require.Error(t, manager.Servers().RepairServerRuntime(server.ID))
	require.True(t, manager.Servers().ServerRuntimeHalted(server.ID))
	fail = false
	require.NoError(t, manager.Servers().RepairServerRuntime(server.ID))
	require.False(t, manager.Servers().ServerRuntimeHalted(server.ID))
	require.True(t, manager.Servers().ServerAvailableForSelection(server.ID))
}

func TestRepairServerRuntimeRejectsHealthyServer(t *testing.T) {
	manager := New(t.TempDir())
	server := &AgentTeamServer{ID: "server-a", Enabled: true}
	require.NoError(t, manager.Servers().Add(server))
	activateTestServerRuntime(t, manager, &testServerRuntimeParticipant{})

	require.ErrorIs(t, manager.Servers().RepairServerRuntime(server.ID), ErrServerRuntimeNotHalted)
}

func TestReloadServersSerializesWithSameServerMutationRollback(t *testing.T) {
	dir := t.TempDir()
	manager := New(dir)
	server := &AgentTeamServer{
		ID:          "server-a",
		Enabled:     true,
		Name:        "Old",
		ServerURL:   "https://old.example.test",
		WorkspaceID: "wsp-old",
	}
	require.NoError(t, manager.Servers().Add(server))

	entered := make(chan struct{})
	release := make(chan struct{})
	calls := 0
	participant := &testServerRuntimeParticipant{
		reconcile: func(previous, current *AgentTeamServer) error {
			calls++
			if calls == 1 {
				close(entered)
				<-release
				return errors.New("identity reconcile failed")
			}
			return nil
		},
	}
	activateTestServerRuntime(t, manager, participant)

	nextWorkspace := "wsp-new"
	cfg, err := yaml.Marshal(legacyAgentTeamConfig{
		ServerURL:   server.ServerURL,
		WorkspaceID: nextWorkspace,
	})
	require.NoError(t, err)
	patchDone := make(chan error, 1)
	go func() {
		_, patchErr := manager.Servers().Patch(server.ID, ServerUpdate{
			Config: rawMessagePointer(cfg),
		})
		patchDone <- patchErr
	}()
	<-entered

	metadata := *server
	metadata.Name = "Accepted metadata"
	writeTestServersFile(t, dir, []AgentTeamServer{metadata})
	reloadDone := make(chan error, 1)
	reloadStarted := make(chan struct{})
	go func() {
		close(reloadStarted)
		reloadDone <- manager.ReloadServersChecked()
	}()
	<-reloadStarted
	select {
	case err := <-reloadDone:
		t.Fatalf("same-server reload bypassed the mutation lock: %v", err)
	default:
	}

	close(release)
	require.ErrorContains(t, <-patchDone, "identity reconcile failed")
	require.NoError(t, <-reloadDone)
	current, ok := manager.Servers().Get(server.ID)
	require.True(t, ok)
	require.Equal(t, "Accepted metadata", current.Name)
	require.Equal(t, server.WorkspaceID, current.WorkspaceID)
}

func TestPersistRollbackFailureFailsClosed(t *testing.T) {
	manager := New(t.TempDir())
	server := &AgentTeamServer{ID: "server-a", Enabled: true, WorkspaceID: "wsp-old"}
	require.NoError(t, manager.Servers().Add(server))
	participant := &testServerRuntimeParticipant{
		reconcile: func(*AgentTeamServer, *AgentTeamServer) error {
			return errors.New("reconcile failed")
		},
	}
	activateTestServerRuntime(t, manager, participant)
	require.NoError(t, failpoint.Enable("profile-server-persist-rollback", `return("disk restore failed")`))
	t.Cleanup(func() {
		require.NoError(t, failpoint.Disable("profile-server-persist-rollback"))
	})

	cfg, err := yaml.Marshal(legacyAgentTeamConfig{WorkspaceID: "wsp-new"})
	require.NoError(t, err)
	_, err = manager.Servers().Patch(server.ID, ServerUpdate{Config: rawMessagePointer(cfg)})
	require.ErrorContains(t, err, "config rollback failed")
	require.True(t, manager.Servers().ServerRuntimeHalted(server.ID))
	require.Equal(t, []string{server.ID}, participant.invalidated)
	require.Zero(t, participant.transition.completed)
	require.Equal(t, 1, participant.transition.failClosed)
}

func TestServerMutationsSerializePerServerButNotAcrossServers(t *testing.T) {
	manager := New(t.TempDir())
	for _, id := range []string{"server-a", "server-b"} {
		server := &AgentTeamServer{ID: id, Enabled: true, WorkspaceID: "old-" + id}
		require.NoError(t, manager.Servers().Add(server))
	}
	participant := &blockingServerTransitionParticipant{
		testServerRuntimeParticipant: &testServerRuntimeParticipant{},
		entered:                      make(chan string, 3),
		release: map[string]chan struct{}{
			"server-a": make(chan struct{}),
			"server-b": make(chan struct{}),
		},
	}
	require.NoError(t, manager.Servers().PrepareRuntimeMode())
	require.NoError(t, manager.Servers().ActivateRuntimeMode(participant))
	t.Cleanup(manager.Servers().DeactivateRuntimeMode)

	patch := func(id, workspace string, done chan<- error) {
		cfg, err := yaml.Marshal(legacyAgentTeamConfig{WorkspaceID: workspace})
		if err != nil {
			done <- err
			return
		}
		_, err = manager.Servers().Patch(id, ServerUpdate{Config: rawMessagePointer(cfg)})
		done <- err
	}
	doneA1 := make(chan error, 1)
	doneA2 := make(chan error, 1)
	doneB := make(chan error, 1)
	go patch("server-a", "a-1", doneA1)
	require.Equal(t, "server-a", <-participant.entered)
	require.False(t, manager.Servers().ServerAvailableForSelection("server-a"))
	require.True(t, manager.Servers().ServerAvailableForSelection("server-b"))
	go patch("server-a", "a-2", doneA2)
	go patch("server-b", "b-1", doneB)
	require.Equal(t, "server-b", <-participant.entered)
	select {
	case id := <-participant.entered:
		t.Fatalf("second same-server mutation entered before first completed: %s", id)
	default:
	}
	close(participant.release["server-b"])
	require.NoError(t, <-doneB)
	close(participant.release["server-a"])
	require.NoError(t, <-doneA1)
	require.Equal(t, "server-a", <-participant.entered)
	require.NoError(t, <-doneA2)
}

func TestRollbackByServerIDPreservesConcurrentOtherServerCommit(t *testing.T) {
	manager := New(t.TempDir())
	for _, id := range []string{"server-a", "server-b"} {
		server := &AgentTeamServer{ID: id, Enabled: true, WorkspaceID: "old-" + id}
		require.NoError(t, manager.Servers().Add(server))
	}
	bCommitted := make(chan struct{})
	var callsMu sync.Mutex
	aCalls := 0
	participant := &testServerRuntimeParticipant{}
	participant.reconcile = func(_ *AgentTeamServer, current *AgentTeamServer) error {
		if current == nil {
			return nil
		}
		switch current.ID {
		case "server-b":
			close(bCommitted)
			return nil
		case "server-a":
			callsMu.Lock()
			aCalls++
			call := aCalls
			callsMu.Unlock()
			if call == 1 {
				<-bCommitted
				return errors.New("server-a reconcile failed")
			}
		}
		return nil
	}
	activateTestServerRuntime(t, manager, participant)

	patch := func(id, workspace string, done chan<- error) {
		cfg, err := yaml.Marshal(legacyAgentTeamConfig{WorkspaceID: workspace})
		if err == nil {
			_, err = manager.Servers().Patch(id, ServerUpdate{Config: rawMessagePointer(cfg)})
		}
		done <- err
	}
	doneA := make(chan error, 1)
	doneB := make(chan error, 1)
	go patch("server-a", "new-a", doneA)
	go patch("server-b", "new-b", doneB)
	require.NoError(t, <-doneB)
	require.ErrorContains(t, <-doneA, "server-a reconcile failed")

	serverA, ok := manager.Servers().Get("server-a")
	require.True(t, ok)
	require.Equal(t, "old-server-a", serverA.WorkspaceID)
	serverB, ok := manager.Servers().Get("server-b")
	require.True(t, ok)
	require.Equal(t, "new-b", serverB.WorkspaceID)
}

func TestReloadServersRejectsIdentityButAcceptsMetadata(t *testing.T) {
	dir := t.TempDir()
	manager := New(dir)
	server := &AgentTeamServer{
		ID:          "server-a",
		Enabled:     true,
		Name:        "Old",
		ServerURL:   "https://old.example.test",
		WorkspaceID: "wsp-old",
	}
	require.NoError(t, manager.Servers().Add(server))
	activateTestServerRuntime(t, manager, &testServerRuntimeParticipant{})

	identityEdit := *server
	identityEdit.ServerURL = "https://evil.example.test"
	writeTestServersFile(t, dir, []AgentTeamServer{identityEdit})
	require.ErrorIs(t, manager.ReloadServersChecked(), ErrExternalServerIdentityChange)
	current, ok := manager.Servers().Get(server.ID)
	require.True(t, ok)
	require.Equal(t, server.ServerURL, current.ServerURL)
	onDisk := loadTestServersFile(t, dir)
	require.Equal(t, server.ServerURL, onDisk[0].ServerURL)
	rejectedMatches, err := filepath.Glob(filepath.Join(dir, serversFileName+".rejected-*"))
	require.NoError(t, err)
	require.Len(t, rejectedMatches, 1)
	rejectedInfo, err := os.Stat(rejectedMatches[0])
	require.NoError(t, err)
	if runtime.GOOS != "windows" {
		require.Equal(t, os.FileMode(0o600), rejectedInfo.Mode().Perm())
	}
	rejectedFile := &serversFile{}
	require.NoError(t, loadYAMLFile(rejectedMatches[0], rejectedFile))
	require.Equal(t, "https://evil.example.test", rejectedFile.Servers[0].ServerURL)

	metadataEdit := *server
	metadataEdit.Name = "Renamed"
	metadataEdit.Description = "Updated"
	writeTestServersFile(t, dir, []AgentTeamServer{metadataEdit})
	require.NoError(t, manager.ReloadServersChecked())
	current, ok = manager.Servers().Get(server.ID)
	require.True(t, ok)
	require.Equal(t, "Renamed", current.Name)
	require.Equal(t, "Updated", current.Description)
}

func TestWatcherSelfIgnoreIsRevisionScoped(t *testing.T) {
	dir := t.TempDir()
	manager := New(dir)
	require.NoError(t, manager.StartWatcher())
	t.Cleanup(manager.StopWatcher)
	path := filepath.Join(dir, serversFileName)

	h1 := []byte("servers:\n  - id: server-a\n    enabled: true\n")
	h2 := []byte("servers:\n  - id: server-b\n    enabled: true\n")
	manager.watcher.SuppressRevision(path, sha256.Sum256(h1))
	require.NoError(t, os.WriteFile(path, h2, 0o600))

	manager.watcher.onFileChanged(path)
	manager.watcher.mu.Lock()
	_, stillSuppressed := manager.watcher.selfWrites[path]
	timer := manager.watcher.debounce[path]
	if timer != nil {
		timer.Stop()
	}
	manager.watcher.mu.Unlock()
	require.False(t, stillSuppressed, "a later H2 must not be consumed as self-written H1")
}

func TestRejectedReloadRevisionCASReevaluatesLaterH2(t *testing.T) {
	dir := t.TempDir()
	manager := New(dir)
	server := &AgentTeamServer{
		ID:        "server-a",
		Enabled:   true,
		Name:      "Old",
		ServerURL: "https://old.example.test",
	}
	require.NoError(t, manager.Servers().Add(server))
	activateTestServerRuntime(t, manager, &testServerRuntimeParticipant{})
	path := filepath.Join(dir, serversFileName)

	rejected := *server
	rejected.ServerURL = "https://rejected.example.test"
	writeTestServersFile(t, dir, []AgentTeamServer{rejected})
	h1, err := os.ReadFile(path)
	require.NoError(t, err)

	h2 := *server
	h2.Name = "Later Legal Metadata"
	writeTestServersFile(t, dir, []AgentTeamServer{h2})
	require.ErrorIs(
		t,
		manager.restoreRejectedServersRevision(path, h1, []AgentTeamServer{*server}),
		errServerRevisionChanged,
	)
	require.NoError(t, manager.ReloadServersChecked())
	current, ok := manager.Servers().Get(server.ID)
	require.True(t, ok)
	require.Equal(t, "Later Legal Metadata", current.Name)
	require.Equal(t, server.ServerURL, current.ServerURL)
}

func TestServerMutationEntrypointsDoNotBypassRuntimeOwner(t *testing.T) {
	_, thisFile, _, ok := runtime.Caller(0)
	require.True(t, ok)
	internalRoot := filepath.Dir(filepath.Dir(thisFile))
	var violations []string
	err := filepath.WalkDir(internalRoot, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			return nil
		}
		if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
			return nil
		}
		data, readErr := os.ReadFile(path)
		if readErr != nil {
			return readErr
		}
		source := string(data)
		for _, forbidden := range []string{
			".Servers().Update(",
			".Servers().ReplaceServer(",
			".Servers().Add(",
			".Servers().Delete(",
			".Servers().SetEnabled(",
		} {
			if !strings.Contains(source, forbidden) {
				continue
			}
			relative, relErr := filepath.Rel(internalRoot, path)
			if relErr != nil {
				relative = path
			}
			if relative == filepath.Join("operator", "file_server_operator.go") ||
				strings.HasPrefix(relative, "cmd"+string(filepath.Separator)) {
				continue
			}
			violations = append(violations, relative+": "+forbidden)
		}
		return nil
	})
	require.NoError(t, err)
	require.Empty(t, violations, "production callers must use field-level owner APIs: %v", violations)
}

func rawMessagePointer(value []byte) *types.RawMessage {
	raw := types.RawMessage(value)
	return &raw
}

func writeTestServersFile(t *testing.T, dir string, servers []AgentTeamServer) {
	t.Helper()
	data, err := yaml.Marshal(serversFile{Servers: servers})
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(dir, serversFileName), data, 0o600))
}

func loadTestServersFile(t *testing.T, dir string) []AgentTeamServer {
	t.Helper()
	file := &serversFile{}
	require.NoError(t, loadYAMLFile(filepath.Join(dir, serversFileName), file))
	return file.Servers
}
