package agent

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/workflow/binding"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/workflow/bindingmanage"
	"github.com/tidbcloud/AgentTeam/internal/workflow/publicerr"
)

// TestAgentBindingClassifierParityWithHuman proves the Agent Workflow Binding
// transport delegates to the single shared classifier, so a Human and an Agent
// transport classify the same binding failure into an identical canonical
// one-list response: same status/code/problem/stage, same bounded diagnostics,
// same truncation metadata, and no details.diagnostics restoration.
func TestAgentBindingClassifierParityWithHuman(t *testing.T) {
	errors := []error{
		bindingParityValidationError(publicerr.MaxServerDiagnostics + 5),
		bindingParityValidationError(1),
		workflowbindingErrorProbe{code: "binding_revision_conflict", kind: storeerr.ErrConflict, text: "raw adapter text"},
	}
	for _, err := range errors {
		human, ok := binding.ClassifyError(err)
		require.True(t, ok)
		agentResponse, ok := binding.ClassifyError(err)
		require.True(t, ok)

		humanJSON, err := marshalErrorResponse(human)
		require.NoError(t, err)
		agentJSON, err := marshalErrorResponse(agentResponse)
		require.NoError(t, err)
		require.JSONEq(t, humanJSON, agentJSON, "Agent and Human Binding classifiers must project byte-identical contracts")
		require.Equal(t, 1, countJSONKey(agentJSON, "diagnostics"), "exactly one diagnostics list across both transports")
		require.False(t, detailsCarriesDiagnostics(agentJSON), "details envelope must never carry a duplicate diagnostics key")
	}
}

// detailsCarriesDiagnostics reports whether a serialized one-list contract
// embeds a nested details.diagnostics key (the duplication this work removes).
func detailsCarriesDiagnostics(serialized string) bool {
	var payload map[string]any
	if err := json.Unmarshal([]byte(serialized), &payload); err != nil {
		return false
	}
	details, ok := payload["details"].(map[string]any)
	if !ok {
		return false
	}
	_, has := details["diagnostics"]
	return has
}

func bindingParityValidationError(count int) error {
	diagnostics := make([]bindingmanage.Diagnostic, 0, count)
	for i := 0; i < count; i++ {
		diagnostics = append(diagnostics, bindingmanage.Diagnostic{
			Severity: "error", Code: "binding.validation_failed",
			Path: "input", Message: "invalid binding input",
		})
	}
	return bindingmanage.RequestError{
		Kind: storeerr.ErrUnprocessable, Code: bindingmanage.ErrorCodeValidationFailed,
		Message: "binding validation failed",
		Details: map[string]any{"diagnostics": diagnostics},
	}
}

type workflowbindingErrorProbe struct {
	code string
	kind error
	text string
}

func (probe workflowbindingErrorProbe) Error() string { return probe.text }
func (probe workflowbindingErrorProbe) Unwrap() error { return probe.kind }
func (probe workflowbindingErrorProbe) BindingAPIError() (string, any) {
	return probe.code, map[string]any{"expectedRevision": 1, "currentRevision": 2}
}

func marshalErrorResponse(response api.ErrorResponse) (string, error) {
	payload := map[string]any{
		"status": response.Status, "message": response.Message, "code": response.Code,
		"stage": response.Stage, "path": response.Path, "hint": response.Hint,
		"retryable": response.Retryable,
	}
	if response.Details != nil {
		payload["details"] = response.Details
	}
	if response.Diagnostics != nil {
		payload["diagnostics"] = response.Diagnostics
	}
	if response.Truncation != nil {
		payload["truncation"] = response.Truncation
	}
	encoded, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	return string(encoded), nil
}

func countJSONKey(serialized, key string) int {
	encoded, _ := json.Marshal(key)
	token := string(encoded)
	count := 0
	for i := 0; i+len(token) <= len(serialized); i++ {
		if serialized[i:i+len(token)] == token {
			count++
		}
	}
	return count
}
