package acp_test

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/acp"
	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

// fakeAgentKindRepo 是仅覆盖 AgentKind 方法的 fake；其他方法 panic
// （隔离单测，不引 testcontainers）。
type fakeAgentKindRepo struct {
	store map[uuid.UUID]*acp.AgentKind
	files map[string]*acp.ConfigFile // key: kindID/relPath
}

func newFakeAgentKindRepo() *fakeAgentKindRepo {
	return &fakeAgentKindRepo{
		store: map[uuid.UUID]*acp.AgentKind{},
		files: map[string]*acp.ConfigFile{},
	}
}

func (f *fakeAgentKindRepo) CreateAgentKind(_ context.Context, k *acp.AgentKind) (*acp.AgentKind, error) {
	cp := *k
	f.store[k.ID] = &cp
	return &cp, nil
}
func (f *fakeAgentKindRepo) GetAgentKindByID(_ context.Context, id uuid.UUID) (*acp.AgentKind, error) {
	if k, ok := f.store[id]; ok {
		cp := *k
		return &cp, nil
	}
	return nil, errs.New(errs.CodeAcpAgentKindNotFound, "not found")
}
func (f *fakeAgentKindRepo) GetAgentKindByName(_ context.Context, name string) (*acp.AgentKind, error) {
	for _, k := range f.store {
		if k.Name == name {
			cp := *k
			return &cp, nil
		}
	}
	return nil, errs.New(errs.CodeAcpAgentKindNotFound, "not found")
}
func (f *fakeAgentKindRepo) ListAgentKinds(_ context.Context) ([]*acp.AgentKind, error) {
	out := make([]*acp.AgentKind, 0, len(f.store))
	for _, k := range f.store {
		cp := *k
		out = append(out, &cp)
	}
	return out, nil
}
func (f *fakeAgentKindRepo) ListEnabledAgentKinds(_ context.Context) ([]*acp.AgentKind, error) {
	out := make([]*acp.AgentKind, 0)
	for _, k := range f.store {
		if k.Enabled {
			cp := *k
			out = append(out, &cp)
		}
	}
	return out, nil
}
func (f *fakeAgentKindRepo) UpdateAgentKind(_ context.Context, k *acp.AgentKind) (*acp.AgentKind, error) {
	if _, ok := f.store[k.ID]; !ok {
		return nil, errs.New(errs.CodeAcpAgentKindNotFound, "not found")
	}
	cp := *k
	f.store[k.ID] = &cp
	return &cp, nil
}
func (f *fakeAgentKindRepo) DeleteAgentKind(_ context.Context, id uuid.UUID) error {
	delete(f.store, id)
	return nil
}
func (f *fakeAgentKindRepo) CountAgentKindUsage(_ context.Context, _ uuid.UUID) (int64, error) {
	return 0, nil
}
func (f *fakeAgentKindRepo) ListConfigFiles(_ context.Context, kindID uuid.UUID) ([]*acp.ConfigFile, error) {
	out := make([]*acp.ConfigFile, 0)
	for _, cf := range f.files {
		if cf.AgentKindID == kindID {
			cp := *cf
			out = append(out, &cp)
		}
	}
	return out, nil
}
func (f *fakeAgentKindRepo) UpsertConfigFile(_ context.Context, cf *acp.ConfigFile) (*acp.ConfigFile, error) {
	cp := *cf
	f.files[cf.AgentKindID.String()+"/"+cf.RelPath] = &cp
	return &cp, nil
}
func (f *fakeAgentKindRepo) DeleteConfigFile(_ context.Context, kindID uuid.UUID, relPath string) (bool, error) {
	key := kindID.String() + "/" + relPath
	if _, ok := f.files[key]; !ok {
		return false, nil
	}
	delete(f.files, key)
	return true, nil
}

// 其他方法 panic（不该被 AgentKindService 调用）
func (f *fakeAgentKindRepo) InsertSession(context.Context, *acp.Session) (*acp.Session, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) GetSessionByID(context.Context, uuid.UUID) (*acp.Session, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) GetSessionByStepID(context.Context, uuid.UUID) (*acp.Session, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) GetCrashedSessionForResume(context.Context, uuid.UUID) (*acp.Session, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) ResetSessionForResume(context.Context, uuid.UUID) error {
	panic("n/a")
}
func (f *fakeAgentKindRepo) ListSessionsByUser(context.Context, uuid.UUID, *uuid.UUID) ([]*acp.Session, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) ListAllSessions(context.Context, *uuid.UUID) ([]*acp.Session, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) UpdateSessionRunning(context.Context, uuid.UUID, string, int32) error {
	panic("n/a")
}
func (f *fakeAgentKindRepo) UpdateSessionSandboxMode(context.Context, uuid.UUID, string) error {
	panic("n/a")
}
func (f *fakeAgentKindRepo) UpdateSessionFinished(context.Context, uuid.UUID, acp.SessionStatus, *int32, *string) error {
	panic("n/a")
}
func (f *fakeAgentKindRepo) MarkSessionFailedIfActive(context.Context, uuid.UUID, string) (bool, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) CountActiveSessions(context.Context) (int64, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) CountActiveSessionsByUser(context.Context, uuid.UUID) (int64, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) ResetStuckSessionsOnRestart(context.Context) ([]*acp.Session, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) AcquireMainWorktree(context.Context, uuid.UUID, uuid.UUID) (bool, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) ReleaseMainWorktree(context.Context, uuid.UUID, uuid.UUID) (bool, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) InsertEvent(context.Context, *acp.Event) (*acp.Event, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) ListEventsSince(context.Context, uuid.UUID, int64, int32) ([]*acp.Event, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) PurgeEventsBefore(context.Context, time.Time) (int64, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) InsertSessionUsage(context.Context, *acp.SessionUsageRecord) (bool, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) AddSessionUsageTotals(context.Context, uuid.UUID, int64, int64, int64, float64) (int64, int64, int64, float64, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) SumProjectCostUSD(context.Context, uuid.UUID, time.Time) (float64, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) MarkSessionTerminatedReason(context.Context, uuid.UUID, string) error {
	panic("n/a")
}
func (f *fakeAgentKindRepo) ListSessionsForReaper(context.Context, time.Time, time.Time) ([]acp.ReaperSession, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) ListSessionUsage(context.Context, uuid.UUID, int32) ([]*acp.SessionUsageRecord, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) SummarizeAcpUsageByUser(context.Context, time.Time, time.Time) ([]acp.AcpUsageUserRow, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) SummarizeAcpUsageByModel(context.Context, time.Time, time.Time) ([]acp.AcpUsageModelRow, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) SummarizeAcpUsageByDay(context.Context, time.Time, time.Time) ([]acp.AcpUsageDayRow, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) SessionRollup(context.Context, acp.DashboardFilter) (acp.SessionRollup, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) SessionsByDay(context.Context, acp.DashboardFilter) ([]acp.SessionDayRow, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) SessionTimeline(context.Context, uuid.UUID) ([]acp.SessionTimelineBucket, error) {
	panic("n/a")
}

func (f *fakeAgentKindRepo) InsertTurn(context.Context, *acp.Turn) (*acp.Turn, error) { panic("n/a") }
func (f *fakeAgentKindRepo) MarkTurnCompleted(context.Context, uuid.UUID, int, string, int) error {
	panic("n/a")
}
func (f *fakeAgentKindRepo) MarkTurnAborted(context.Context, uuid.UUID, int) error { panic("n/a") }
func (f *fakeAgentKindRepo) IncrementTurnUpdateCount(context.Context, uuid.UUID, int) error {
	panic("n/a")
}
func (f *fakeAgentKindRepo) GetLatestTurnBySession(context.Context, uuid.UUID) (*acp.Turn, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) ListTurnsBySession(context.Context, uuid.UUID) ([]*acp.Turn, error) {
	panic("n/a")
}
func (f *fakeAgentKindRepo) UpdateSessionLastStopReason(context.Context, uuid.UUID, string) error {
	panic("n/a")
}
func (f *fakeAgentKindRepo) PurgeTurnsBefore(context.Context, time.Time) (int64, error) {
	panic("n/a")
}

func (f *fakeAgentKindRepo) InTx(context.Context, func(context.Context, pgx.Tx) error) error {
	panic("n/a")
}

func (f *fakeAgentKindRepo) WithTx(pgx.Tx) acp.Repository {
	return f
}

// agentKindRecordingRecorder：accumulate audit entries
type agentKindRecordingRecorder struct{ entries []audit.Entry }

func (r *agentKindRecordingRecorder) Record(_ context.Context, e audit.Entry) error {
	r.entries = append(r.entries, e)
	return nil
}

// testEncryptor returns a real *crypto.Encryptor with a fixed test key.
func testEncryptor(t *testing.T) *crypto.Encryptor {
	t.Helper()
	key := []byte("0123456789abcdef0123456789abcdef") // 32 bytes
	enc, err := crypto.NewEncryptor(key)
	require.NoError(t, err)
	return enc
}

func TestAgentKindService_Create(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	repo := newFakeAgentKindRepo()
	rec := &agentKindRecordingRecorder{}
	svc := acp.NewAgentKindService(repo, testEncryptor(t), rec)

	admin := acp.Caller{UserID: uuid.New(), IsAdmin: true}
	k, err := svc.Create(ctx, admin, acp.CreateAgentKindInput{
		Name: "claude_code", DisplayName: "Claude Code",
		BinaryPath: "claude-code", Args: []string{"--acp"},
		Env: map[string]string{"K": "v"}, Enabled: true,
	})
	require.NoError(t, err)
	assert.Equal(t, "claude_code", k.Name)
	assert.NotEmpty(t, k.EncryptedEnv) // env 已加密

	require.Len(t, rec.entries, 1)
	assert.Equal(t, "acp.agent_kind.create", rec.entries[0].Action)
}

func TestAgentKindService_Create_NonAdmin(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	svc := acp.NewAgentKindService(newFakeAgentKindRepo(), testEncryptor(t), &agentKindRecordingRecorder{})

	user := acp.Caller{UserID: uuid.New(), IsAdmin: false}
	_, err := svc.Create(ctx, user, acp.CreateAgentKindInput{
		Name: "x", DisplayName: "x", BinaryPath: "x", Enabled: true,
	})
	ae, ok := errs.As(err)
	require.True(t, ok)
	assert.Equal(t, errs.CodeForbidden, ae.Code)
}

func TestAgentKindService_List_NonAdmin_OnlyEnabled(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	repo := newFakeAgentKindRepo()
	svc := acp.NewAgentKindService(repo, testEncryptor(t), nil)
	admin := acp.Caller{UserID: uuid.New(), IsAdmin: true}
	user := acp.Caller{UserID: uuid.New(), IsAdmin: false}

	_, _ = svc.Create(ctx, admin, acp.CreateAgentKindInput{
		Name: "k1", DisplayName: "k1", BinaryPath: "x", Enabled: true,
	})
	_, _ = svc.Create(ctx, admin, acp.CreateAgentKindInput{
		Name: "k2", DisplayName: "k2", BinaryPath: "x", Enabled: false,
	})

	adminList, _ := svc.List(ctx, admin)
	userList, _ := svc.List(ctx, user)
	assert.Len(t, adminList, 2)
	assert.Len(t, userList, 1)
}

func TestAgentKindService_Update_PatchSemantics(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	repo := newFakeAgentKindRepo()
	rec := &agentKindRecordingRecorder{}
	svc := acp.NewAgentKindService(repo, testEncryptor(t), rec)
	admin := acp.Caller{UserID: uuid.New(), IsAdmin: true}

	k, _ := svc.Create(ctx, admin, acp.CreateAgentKindInput{
		Name: "k", DisplayName: "K", BinaryPath: "/x", Enabled: true,
	})

	disp := "Renamed"
	updated, err := svc.Update(ctx, admin, k.ID, acp.UpdateAgentKindInput{DisplayName: &disp})
	require.NoError(t, err)
	assert.Equal(t, "Renamed", updated.DisplayName)
	assert.Equal(t, "/x", updated.BinaryPath) // 未改

	last := rec.entries[len(rec.entries)-1]
	assert.Equal(t, "acp.agent_kind.update", last.Action)
}

func TestSupervisor_BuildSpawnEnv_ExtraEnvOverrides(t *testing.T) {
	t.Parallel()
	enc := testEncryptor(t)
	plain, err := json.Marshal(map[string]string{"ACW_MCP_TOKEN": "kind-value", "OTHER": "kept"})
	require.NoError(t, err)
	encrypted, err := enc.Encrypt(plain)
	require.NoError(t, err)

	kind := &acp.AgentKind{EncryptedEnv: encrypted}
	sup := acp.NewSupervisor(nil, nil, nil, nil, nil, nil, enc, acp.SupervisorConfig{}, nil)

	env, err := sup.BuildSpawnEnv(kind, map[string]string{
		"ACW_MCP_TOKEN": "system-value",
		"ACW_MCP_URL":   "http://x",
	})
	require.NoError(t, err)
	assert.Contains(t, env, "ACW_MCP_TOKEN=system-value")
	var hasOld bool
	for _, e := range env {
		if e == "ACW_MCP_TOKEN=kind-value" {
			hasOld = true
		}
	}
	assert.False(t, hasOld, "kind value must be overridden")
	assert.Contains(t, env, "OTHER=kept")
	assert.Contains(t, env, "ACW_MCP_URL=http://x")
}

// ===== PermissionRequest（权限审批框架，测试桩） =====
func (f *fakeAgentKindRepo) InsertPermissionRequest(context.Context, *acp.PermissionRequest) (*acp.PermissionRequest, error) {
	return &acp.PermissionRequest{}, nil
}
func (f *fakeAgentKindRepo) GetPermissionRequestByID(context.Context, uuid.UUID) (*acp.PermissionRequest, error) {
	return nil, nil
}
func (f *fakeAgentKindRepo) ListPendingPermissionRequestsBySession(context.Context, uuid.UUID) ([]*acp.PermissionRequest, error) {
	return nil, nil
}
func (f *fakeAgentKindRepo) ListPendingPermissionRequestsByUser(context.Context, uuid.UUID) ([]*acp.PermissionRequest, error) {
	return nil, nil
}
func (f *fakeAgentKindRepo) DecidePermissionRequest(context.Context, uuid.UUID, acp.PermissionStatus, *string, *uuid.UUID) (*acp.PermissionRequest, bool, error) {
	return nil, false, nil
}
func (f *fakeAgentKindRepo) ExpirePendingPermissionRequestsBySession(context.Context, uuid.UUID) (int64, error) {
	return 0, nil
}
func (f *fakeAgentKindRepo) ExpireStalePermissionRequests(context.Context, time.Time) (int64, error) {
	return 0, nil
}
