package publicerr

import (
	"encoding/json"
	"fmt"
	"net/http"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

func TestClassifyDetailsRewritingIsCopyOnWrite(t *testing.T) {
	index := 0
	diagnostics := make([]Diagnostic, 0, MaxServerDiagnostics+3)
	for i := 0; i < MaxServerDiagnostics+3; i++ {
		diagnostics = append(diagnostics, Diagnostic{
			Severity: SeverityError, Code: "plan.test", Path: "$.nodes.n",
			Field: "n", Index: &index, Message: "m",
		})
	}
	details := map[string]any{"diagnostics": diagnostics, "resource": "planDraft"}
	classified, ok := Classify(testDomainError{
		kind: storeerr.ErrUnprocessable, code: "plan_compile_failed", message: "compile failed",
		details: details,
	}, Options{Stage: StagePlan, DefaultCode: "plan_request_failed"})
	require.True(t, ok)
	// The caller's details map is never mutated: the diagnostics key survives
	// untouched and the resource field is preserved.
	require.Len(t, details["diagnostics"].([]Diagnostic), len(diagnostics))
	require.Equal(t, "planDraft", details["resource"])
	// The classified details envelope drops the duplicate diagnostics but keeps
	// the non-diagnostic fields.
	classifiedDetails, ok := classified.Details.(map[string]any)
	require.True(t, ok)
	require.Equal(t, "planDraft", classifiedDetails["resource"])
	require.NotContains(t, classifiedDetails, "diagnostics")
}

func TestClassifyProtectedSelectorsCollapseMissingAndForbidden(t *testing.T) {
	options := Options{
		Stage:       StageBinding,
		DefaultCode: "binding_request_failed",
		ProtectedSelectors: map[string]string{
			"binding_not_found":      "binding_not_found",
			"binding_forbidden":      "binding_not_found",
			"binding_plan_not_found": "binding_not_found",
		},
	}
	missing, ok := Classify(testDomainError{
		kind: storeerr.ErrNotFound, code: "binding_not_found", message: "Workflow Binding bnd_secret is missing",
	}, options)
	require.True(t, ok)
	forbidden, ok := Classify(testDomainError{
		kind: storeerr.ErrForbidden, code: "binding_forbidden", message: "Workflow Binding bnd_secret is forbidden",
	}, options)
	require.True(t, ok)

	// Externally identical: same 404 status, code, problem, details, and all
	// public fields.
	require.Equal(t, http.StatusNotFound, missing.Status)
	require.Equal(t, http.StatusNotFound, forbidden.Status)
	require.Equal(t, missing.Error, forbidden.Error)
	require.Equal(t, "binding_not_found", missing.Error.Code)
	require.NotContains(t, missing.Error.Problem, "bnd_secret")
	require.NotContains(t, forbidden.Error.Problem, "bnd_secret")
	require.Empty(t, missing.Error.Path)
	require.Empty(t, missing.Error.Hint)
	require.False(t, missing.Error.Retryable)
	require.Empty(t, missing.Error.Diagnostics)
	require.Nil(t, missing.Details)
	require.Nil(t, forbidden.Details)
	require.True(t, missing.Collapsed)
	require.True(t, forbidden.Collapsed)

	// Internal audit classification is retained: the true code distinguishes
	// the missing selector from the cross-tenant/forbidden one.
	require.Equal(t, "binding_not_found", missing.InternalCode)
	require.Equal(t, "binding_forbidden", forbidden.InternalCode)
	require.NotEqual(t, missing.InternalCode, forbidden.InternalCode)
}

func TestClassifyProtectedSelectorsCrossTenantPlanSelector(t *testing.T) {
	options := Options{
		Stage:       StagePlan,
		DefaultCode: "plan_request_failed",
		ProtectedSelectors: map[string]string{
			"plan_not_found": "plan_not_found",
		},
	}
	// A cross-tenant Plan selector surfaces as a domain not-found error in the
	// owning workspace and must collapse to the identical bounded contract.
	classified, ok := Classify(testDomainError{
		kind: storeerr.ErrNotFound, code: "plan_not_found", message: "Workflow Plan pln_secret is missing",
	}, options)
	require.True(t, ok)
	require.Equal(t, http.StatusNotFound, classified.Status)
	require.Equal(t, "plan_not_found", classified.Error.Code)
	require.Equal(t, StagePlan, classified.Error.Stage)
	require.NotContains(t, classified.Error.Problem, "pln_secret")
	require.True(t, classified.Collapsed)
	require.Equal(t, "plan_not_found", classified.InternalCode)
}

func TestClassifyProtectedSelectorsKeepNonProtectedForbidden(t *testing.T) {
	options := Options{
		Stage:       StagePlan,
		DefaultCode: "plan_request_failed",
		ProtectedSelectors: map[string]string{
			"plan_not_found": "plan_not_found",
		},
	}
	// A 403 that is not a protected-resource selector (for example a plan
	// creator-required action authorization) keeps its deliberate code, status,
	// and problem; it is never collapsed.
	classified, ok := Classify(testDomainError{
		kind: storeerr.ErrForbidden, code: "workflow_plan_creator_required", message: "Only the creator may modify this Plan",
	}, options)
	require.True(t, ok)
	require.Equal(t, http.StatusForbidden, classified.Status)
	require.Equal(t, "workflow_plan_creator_required", classified.Error.Code)
	require.Equal(t, "Only the creator may modify this Plan", classified.Error.Problem)
	require.False(t, classified.Collapsed)
}

func TestClassifyPublicWorkflowErrorProtectedSelectorCollapse(t *testing.T) {
	options := Options{
		Stage:       StageInvocation,
		DefaultCode: "invocation_request_failed",
		ProtectedSelectors: map[string]string{
			"invocation_not_found": "invocation_not_found",
			"invocation_forbidden": "invocation_not_found",
		},
	}
	missing, ok := Classify(WorkflowError{
		Kind: storeerr.ErrNotFound, Code: "invocation_not_found", Problem: "Workflow Invocation wiv_secret is missing",
	}, options)
	require.True(t, ok)
	forbidden, ok := Classify(WorkflowError{
		Kind: storeerr.ErrForbidden, Code: "invocation_forbidden", Problem: "Workflow Invocation wiv_secret is forbidden",
	}, options)
	require.True(t, ok)

	require.Equal(t, http.StatusNotFound, missing.Status)
	require.Equal(t, http.StatusNotFound, forbidden.Status)
	require.Equal(t, missing.Error, forbidden.Error)
	require.Equal(t, "invocation_not_found", missing.Error.Code)
	require.Nil(t, missing.Details)
	require.Nil(t, forbidden.Details)
	require.Equal(t, "invocation_not_found", missing.InternalCode)
	require.Equal(t, "invocation_forbidden", forbidden.InternalCode)
}

func TestClassifyProtectedSelectorsFailClosedUnknown(t *testing.T) {
	options := Options{
		Stage:       StageRun,
		DefaultCode: "run_request_failed",
		ProtectedSelectors: map[string]string{
			"run_not_found": "run_not_found",
			"run_forbidden": "run_not_found",
		},
	}
	// Unknown failures still fail closed with no leak of raw text.
	classified, ok := Classify(fmt.Errorf("raw repository: dial tcp: SELECT secret FROM runs"), options)
	require.True(t, ok)
	require.Equal(t, http.StatusInternalServerError, classified.Status)
	require.Equal(t, "run_request_failed", classified.Error.Code)
	require.NotContains(t, classified.Error.Problem, "secret")
	require.False(t, classified.Collapsed)
	require.Empty(t, classified.InternalCode)
}

// TestClassifyProtectedSelectorsRawWrappedDomainByteIdentical proves a raw
// store NotFound/Forbidden selector, a wrapped public WorkflowError, and a
// wrapped domain RequestError all collapse to ONE byte-identical external
// contract (same 404 status, code, problem, details, and public fields),
// while InternalCode retains the internal audit distinction between a missing
// selector and a cross-tenant/forbidden one.
func TestClassifyProtectedSelectorsRawWrappedDomainByteIdentical(t *testing.T) {
	options := Options{
		Stage:       StageBinding,
		DefaultCode: "binding_request_failed",
		ProtectedSelectors: map[string]string{
			"binding_not_found":      "binding_not_found",
			"binding_forbidden":      "binding_not_found",
			"binding_plan_not_found": "binding_not_found",
		},
		ProtectedNotFoundCode: "binding_not_found",
	}

	rawNotFound, ok := Classify(storeerr.NotFound("Workflow Binding bnd_secret is missing"), options)
	require.True(t, ok)
	rawForbidden, ok := Classify(storeerr.Forbidden("Workflow Binding bnd_secret is forbidden"), options)
	require.True(t, ok)
	wrappedDomain, ok := Classify(testDomainError{
		kind: storeerr.ErrNotFound, code: "binding_not_found", message: "Workflow Binding bnd_secret is missing",
		details: map[string]any{"bindingId": "bnd_secret"},
	}, options)
	require.True(t, ok)
	wrappedForbidden, ok := Classify(testDomainError{
		kind: storeerr.ErrForbidden, code: "binding_forbidden", message: "Workflow Binding bnd_secret is forbidden",
		details: map[string]any{"bindingId": "bnd_secret"},
	}, options)
	require.True(t, ok)
	wrappedPublic, ok := Classify(WorkflowError{
		Kind: storeerr.ErrNotFound, Code: "binding_not_found", Problem: "Workflow Binding bnd_secret is missing",
		Details: map[string]any{"bindingId": "bnd_secret"},
	}, options)
	require.True(t, ok)

	// Byte-identical external contract across all five inputs.
	all := []Classified{rawNotFound, rawForbidden, wrappedDomain, wrappedForbidden, wrappedPublic}
	for _, classified := range all {
		require.Equal(t, http.StatusNotFound, classified.Status)
		require.Equal(t, "binding_not_found", classified.Error.Code)
		require.Equal(t, StageBinding, classified.Error.Stage)
		require.Empty(t, classified.Error.Path)
		require.Empty(t, classified.Error.Hint)
		require.Empty(t, classified.Error.Diagnostics)
		require.False(t, classified.Error.Retryable)
		require.Nil(t, classified.Details)
		require.True(t, classified.Collapsed)
	}
	// Identical serialized bodies.
	encode := func(classified Classified) string {
		payload, err := json.Marshal(classified.Error)
		require.NoError(t, err)
		return string(payload)
	}
	first := encode(all[0])
	for _, classified := range all[1:] {
		require.Equal(t, first, encode(classified))
	}

	// Internal audit distinction retained.
	require.Equal(t, "store.not_found", rawNotFound.InternalCode)
	require.Equal(t, "store.forbidden", rawForbidden.InternalCode)
	require.Equal(t, "binding_not_found", wrappedDomain.InternalCode)
	require.Equal(t, "binding_forbidden", wrappedForbidden.InternalCode)
	require.Equal(t, "binding_not_found", wrappedPublic.InternalCode)
}
