package dto

import (
	"encoding/json"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestFirstPartyDTOJSONTagsAreCamelCase(t *testing.T) {
	_, file, _, ok := runtime.Caller(0)
	require.True(t, ok)
	dir := filepath.Dir(file)

	tagWithUnderscore := regexp.MustCompile(`json:"[^",-]*_[^",]*`)
	allowedUnderscoreTags := map[string]map[string]struct{}{
		// The agent asset access outbox wire is a server/desktop bridge envelope
		// rather than a first-party MeshX UI DTO. Keep that existing transport
		// shape stable while still guarding the rest of memory.go.
		"memory.go": {
			`json:"lease_owner`:     {},
			`json:"lease_seconds`:   {},
			`json:"workspace_ids`:   {},
			`json:"last_error`:      {},
			`json:"next_attempt_at`: {},
			`json:"event_id`:        {},
			`json:"workspace_id`:    {},
			`json:"agent_ids`:       {},
			`json:"asset_types`:     {},
			`json:"occurred_at`:     {},
			`json:"max_attempts`:    {},
		},
		// AGS wiki/MCP compatibility accepts snake_case aliases from external
		// MCP clients while browser-facing KB DTOs stay camelCase.
		"kb.go": {
			`json:"kb_ids`:             {},
			`json:"source_ids`:         {},
			`json:"citation_uri`:       {},
			`json:"source_id`:          {},
			`json:"repo_id`:            {},
			`json:"repo_role`:          {},
			`json:"wiki_slug`:          {},
			`json:"max_results`:        {},
			`json:"citation_id`:        {},
			`json:"citation_ids`:       {},
			`json:"source_label`:       {},
			`json:"max_chars_per_item`: {},
		},
	}
	for _, name := range []string{"kb.go", "memory.go", "skills.go", "automations.go", "roles.go"} {
		t.Run(name, func(t *testing.T) {
			content, err := os.ReadFile(filepath.Join(dir, name))
			require.NoError(t, err)
			matches := tagWithUnderscore.FindAllString(string(content), -1)
			if allowed := allowedUnderscoreTags[name]; len(allowed) > 0 {
				filtered := matches[:0]
				for _, match := range matches {
					if _, ok := allowed[match]; ok {
						continue
					}
					filtered = append(filtered, match)
				}
				matches = filtered
			}
			require.Empty(t, matches)
		})
	}
}

func TestActionsRunnerExternalProtocolKeepsUpstreamCasing(t *testing.T) {
	brokerRequest, err := json.Marshal(ActionsRunnerBrokerSessionRequest{SessionID: "session-1", OwnerName: "owner", Agent: ActionsRunnerAgent{ID: 1, Name: "runner", Version: "1", Status: 1}})
	require.NoError(t, err)
	require.JSONEq(t, `{"sessionId":"session-1","ownerName":"owner","agent":{"id":1,"name":"runner","version":"1","status":1}}`, string(brokerRequest))

	brokerResponse, err := json.Marshal(ActionsRunnerBrokerSessionResponse{SessionID: "session-1", OwnerName: "owner", UseFipsEncryption: true})
	require.NoError(t, err)
	require.JSONEq(t, `{"sessionId":"session-1","ownerName":"owner","useFipsEncryption":true}`, string(brokerResponse))

	message, err := json.Marshal(ActionsRunnerTaskAgentMessage{MessageID: 42, MessageType: "RunnerJobRequest", Body: "{}"})
	require.NoError(t, err)
	require.JSONEq(t, `{"messageId":42,"messageType":"RunnerJobRequest","body":"{}"}`, string(message))

	renewResponse, err := json.Marshal(ActionsRunnerRenewJobResponse{LockedUntil: "2026-06-19T00:00:00Z"})
	require.NoError(t, err)
	require.JSONEq(t, `{"lockedUntil":"2026-06-19T00:00:00Z"}`, string(renewResponse))

	completeRequest := ActionsRunnerCompleteJobRequest{}
	require.NoError(t, json.Unmarshal([]byte(`{"jobId":"job-1","conclusion":"success","stepResults":[{"name":"build"}]}`), &completeRequest))
	require.Equal(t, "job-1", completeRequest.Input().JobID)
	require.Len(t, completeRequest.Input().StepResults, 1)

	bootstrap, err := json.Marshal(ActionsRunnerRegistrationBootstrapRequest{URL: "https://example.test", RunnerEvent: "register"})
	require.NoError(t, err)
	require.JSONEq(t, `{"url":"https://example.test","runner_event":"register"}`, string(bootstrap))
}
