package acp_test

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/acp"
)

// fakeResolver is a stub middleware.SessionResolver that accepts the literal "valid" token.
type fakeResolver struct{ uid uuid.UUID }

func (f fakeResolver) ResolveSession(_ context.Context, token string) (uuid.UUID, bool, error) {
	if token == "valid" {
		return f.uid, true, nil
	}
	return uuid.Nil, false, nil
}

// fakeAdminLookup is a stub AdminLookup keyed by user ID.
type fakeAdminLookup struct{ admin map[uuid.UUID]bool }

func (f fakeAdminLookup) IsAdmin(_ context.Context, uid uuid.UUID) (bool, error) {
	return f.admin[uid], nil
}

// mountHandlerWithRepo lets a test share one fakeAgentKindRepo across multiple
// mounts (e.g. one as admin, one as non-admin) so the data set is consistent.
func mountHandlerWithRepo(t *testing.T, callerUID uuid.UUID, isAdmin bool, repo *fakeAgentKindRepo) chi.Router {
	t.Helper()
	if repo == nil {
		repo = newFakeAgentKindRepo()
	}
	enc := testEncryptor(t)
	svc := acp.NewAgentKindService(repo, enc, &agentKindRecordingRecorder{})

	r := chi.NewRouter()
	h := acp.NewHandler(
		svc,
		nil, // sessSvc — not exercised by agent-kinds tests
		nil, // sup — not exercised by agent-kinds tests
		nil, // repo — not exercised by agent-kinds tests
		nil, // permSvc — not exercised by agent-kinds tests
		fakeResolver{uid: callerUID},
		fakeAdminLookup{admin: map[uuid.UUID]bool{callerUID: isAdmin}},
		enc,
		acp.WSConfig{},
	)
	h.Mount(r)
	return r
}

func doJSON(t *testing.T, r chi.Router, method, path, token string, body any) (*httptest.ResponseRecorder, map[string]any) {
	t.Helper()
	var buf bytes.Buffer
	if body != nil {
		_ = json.NewEncoder(&buf).Encode(body)
	}
	req := httptest.NewRequest(method, path, &buf)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	rr := httptest.NewRecorder()
	r.ServeHTTP(rr, req)
	var out map[string]any
	if rr.Body.Len() > 0 {
		_ = json.Unmarshal(rr.Body.Bytes(), &out)
	}
	return rr, out
}

func TestHandler_AgentKind_Admin_CreateGetList(t *testing.T) {
	t.Parallel()
	uid := uuid.New()
	r := mountHandlerWithRepo(t, uid, true, nil)

	// Create
	rr, body := doJSON(t, r, "POST", "/api/v1/acp/agent-kinds", "valid", map[string]any{
		"name": "claude_code", "display_name": "Claude Code", "binary_path": "claude-code",
		"args": []string{"--acp"}, "env": map[string]string{"K": "v"}, "enabled": true,
	})
	require.Equal(t, http.StatusCreated, rr.Code, "body=%s", rr.Body.String())
	id := body["id"].(string)
	assert.Equal(t, "claude_code", body["name"])
	envKeys := body["env_keys"].([]any)
	assert.Equal(t, []any{"K"}, envKeys)

	// Get
	rr, body = doJSON(t, r, "GET", "/api/v1/acp/agent-kinds/"+id, "valid", nil)
	require.Equal(t, http.StatusOK, rr.Code)
	assert.Equal(t, "Claude Code", body["display_name"])

	// List
	rr, _ = doJSON(t, r, "GET", "/api/v1/acp/agent-kinds", "valid", nil)
	require.Equal(t, http.StatusOK, rr.Code)
}

func TestHandler_AgentKind_NonAdmin_CannotWrite_FieldsRedacted(t *testing.T) {
	t.Parallel()
	uid := uuid.New()
	repo := newFakeAgentKindRepo()

	// Admin creates one kind via the handler so audit + repo state are consistent.
	adminR := mountHandlerWithRepo(t, uid, true, repo)
	rr, _ := doJSON(t, adminR, "POST", "/api/v1/acp/agent-kinds", "valid", map[string]any{
		"name": "claude_code", "display_name": "Claude Code", "binary_path": "claude-code",
		"args": []string{"--acp"}, "env": map[string]string{"K": "v"}, "enabled": true,
	})
	require.Equal(t, http.StatusCreated, rr.Code)

	// Same UID but treated as non-admin (fakeAdminLookup says false).
	userR := mountHandlerWithRepo(t, uid, false, repo)

	// List as non-admin: returns AgentKindPublicDTO array (no binary_path / env_keys).
	rr2 := httptest.NewRecorder()
	req2 := httptest.NewRequest("GET", "/api/v1/acp/agent-kinds", nil)
	req2.Header.Set("Authorization", "Bearer valid")
	userR.ServeHTTP(rr2, req2)
	require.Equal(t, http.StatusOK, rr2.Code)
	var arr []map[string]any
	require.NoError(t, json.Unmarshal(rr2.Body.Bytes(), &arr))
	require.Len(t, arr, 1)
	assert.Equal(t, "claude_code", arr[0]["name"])
	assert.NotContains(t, arr[0], "binary_path")
	assert.NotContains(t, arr[0], "env_keys")
	assert.NotContains(t, arr[0], "args")

	// Non-admin POST → 403 Forbidden from the service layer.
	rr3, _ := doJSON(t, userR, "POST", "/api/v1/acp/agent-kinds", "valid", map[string]any{
		"name": "x", "display_name": "x", "binary_path": "x",
	})
	assert.Equal(t, http.StatusForbidden, rr3.Code)
}

func TestHandler_AgentKind_Update_PatchSemantics(t *testing.T) {
	t.Parallel()
	uid := uuid.New()
	r := mountHandlerWithRepo(t, uid, true, nil)

	rr, body := doJSON(t, r, "POST", "/api/v1/acp/agent-kinds", "valid", map[string]any{
		"name": "k", "display_name": "K", "binary_path": "/x",
	})
	require.Equal(t, http.StatusCreated, rr.Code, "body=%s", rr.Body.String())
	id := body["id"].(string)

	// PATCH display_name only — other fields preserved.
	rr, body = doJSON(t, r, "PATCH", "/api/v1/acp/agent-kinds/"+id, "valid", map[string]any{
		"display_name": "Renamed",
	})
	require.Equal(t, http.StatusOK, rr.Code, "body=%s", rr.Body.String())
	assert.Equal(t, "Renamed", body["display_name"])
	assert.Equal(t, "/x", body["binary_path"])

	// DELETE
	rr, _ = doJSON(t, r, "DELETE", "/api/v1/acp/agent-kinds/"+id, "valid", nil)
	assert.Equal(t, http.StatusNoContent, rr.Code)
}
