package plan

import (
	"bytes"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"

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

const requestApprovalFeedbackScopePlan = `apiVersion: loop.plan/v1
kind: PlanVersion
input:
  feedback_scope: {type: scope, required: true}
  request: {type: string, required: true}
graph:
  entry:
    nodeId: approve
    port: in
    input:
      feedback_scope: {ref: plan.input.feedback_scope}
      request: {ref: plan.input.request}
  nodes:
  - id: approve
    type: request_approval
    input:
      feedback_scope: {type: scope, required: true}
      request: {type: string, required: true}
    requestApproval:
      assignee: alice
      summary: "Approve {{ input.request }}"
      feedbackScope: {ref: input.feedback_scope}
      retry: {maxAttempts: 2, backoff: PT1S}
  - id: success
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: approval_failed}
  edges:
  - id: approved
    source: {nodeId: approve, port: approved}
    target: {nodeId: success, port: in}
  - id: rejected
    source: {nodeId: approve, port: rejected}
    target: {nodeId: failure, port: in}
  - id: failed
    source: {nodeId: approve, port: failed}
    target: {nodeId: failure, port: in}
`

func TestRequestApprovalFeedbackScopeCompilesToV5AndRoundTrips(t *testing.T) {
	t.Parallel()

	artifact := compileArtifactForTest(t, requestApprovalFeedbackScopePlan)
	require.Equal(t, ArtifactVersionV5, artifact.ArtifactVersion)
	require.Equal(t, CompilerVersionV5, artifact.CompilerVersion)
	config := artifactNodeForTest(artifact, "approve").Config.RequestApproval
	require.NotNil(t, config)
	require.NotNil(t, config.FeedbackScope)
	require.Equal(t, TypeScope, config.FeedbackScope.ResultType)
	require.Equal(t, ArtifactOpLoad, config.FeedbackScope.Op)
	require.Equal(t, ArtifactLoadActivationInput, config.FeedbackScope.Load.Space)
	require.Equal(t, []string{"feedback_scope"}, config.FeedbackScope.Load.Path)
	require.NoError(t, ValidateEnvelope(artifact))

	wire, err := artifact.DeterministicJSON()
	require.NoError(t, err)
	decoded, err := DecodeArtifact(wire)
	require.NoError(t, err)
	require.Equal(t, artifact.Digest, decoded.Digest)
	require.NotNil(t, artifactNodeForTest(decoded, "approve").Config.RequestApproval.FeedbackScope)
	require.JSONEq(t, string(wire), mustDeterministicArtifactJSON(t, decoded))
}

func TestRequestApprovalFeedbackScopeLiteralIsCanonicalAndStrict(t *testing.T) {
	t.Parallel()

	literal := strings.Replace(
		requestApprovalFeedbackScopePlan,
		"feedbackScope: {ref: input.feedback_scope}",
		"feedbackScope: {value: {scopeId: tsk_abcdefghijklmn, kind: task}}",
		1,
	)
	artifact := compileArtifactForTest(t, literal)
	require.Equal(t, ArtifactVersionV5, artifact.ArtifactVersion)
	feedbackScope := artifactNodeForTest(artifact, "approve").Config.RequestApproval.FeedbackScope
	require.Equal(t, ArtifactOpConst, feedbackScope.Op)
	require.JSONEq(t, `{"kind":"task","scopeId":"tsk_abcdefghijklmn"}`, string(feedbackScope.Value))

	invalid := []struct {
		name   string
		source string
		code   string
	}{
		{
			name:   "target string",
			source: strings.Replace(literal, "{value: {scopeId: tsk_abcdefghijklmn, kind: task}}", "{value: \"#all\"}", 1),
			code:   "approval.feedback_scope_type",
		},
		{
			name:   "unknown scope field",
			source: strings.Replace(literal, "{value: {scopeId: tsk_abcdefghijklmn, kind: task}}", "{value: {kind: task, scopeId: tsk_abcdefghijklmn, target: '#all'}}", 1),
			code:   "approval.feedback_scope_type",
		},
		{
			name:   "unknown expression field",
			source: strings.Replace(requestApprovalFeedbackScopePlan, "{ref: input.feedback_scope}", "{ref: input.feedback_scope, typo: true}", 1),
			code:   "syntax.unknown_field",
		},
		{
			name:   "null",
			source: strings.Replace(requestApprovalFeedbackScopePlan, "feedbackScope: {ref: input.feedback_scope}", "feedbackScope: null", 1),
			code:   "syntax.type_mismatch",
		},
		{
			name:   "wrong expression type",
			source: strings.Replace(requestApprovalFeedbackScopePlan, "feedbackScope: {ref: input.feedback_scope}", "feedbackScope: {ref: input.request}", 1),
			code:   "approval.feedback_scope_type",
		},
	}
	for _, test := range invalid {
		t.Run(test.name, func(t *testing.T) {
			artifact, diagnostics, err := Compile([]byte(test.source))
			require.NoError(t, err)
			require.Nil(t, artifact)
			require.True(t, hasCode(diagnostics, test.code), "diagnostics: %#v", diagnostics)
		})
	}
}

func TestRequestApprovalFeedbackScopeArtifactVersionGateIsFailClosed(t *testing.T) {
	t.Parallel()

	artifact := compileArtifactForTest(t, requestApprovalFeedbackScopePlan)
	artifact.ArtifactVersion = ArtifactVersionV4
	artifact.CompilerVersion = CompilerVersionV4
	require.ErrorContains(t, ValidateEnvelope(artifact), "does not support v5 Workflow features")

	artifact = compileArtifactForTest(t, requestApprovalFeedbackScopePlan)
	artifact.CompilerVersion = CompilerVersionV4
	require.ErrorContains(t, ValidateEnvelope(artifact), "requires compilerVersion")

	legacy := compileArtifactForTest(t, strings.Replace(requestApprovalFeedbackScopePlan, "\n      feedbackScope: {ref: input.feedback_scope}", "", 1))
	legacy.ArtifactVersion = ArtifactVersionV5
	legacy.CompilerVersion = CompilerVersionV5
	require.ErrorContains(t, ValidateEnvelope(legacy), "requires at least one v5 Workflow feature")
}

func TestRequestApprovalFeedbackScopeOmissionPreservesLegacyArtifactBytes(t *testing.T) {
	t.Parallel()

	legacySource := linearActionPlan("", `  - id: action
    type: request_approval
    requestApproval:
      assignee: alice
      summary: Approve this run
      retry: {maxAttempts: 2, backoff: 1s}`, []string{"approved", "rejected", "failed"})
	artifact := compileArtifactForTest(t, legacySource)
	require.Equal(t, ArtifactVersionV1, artifact.ArtifactVersion)
	require.Equal(t, CompilerVersionV1, artifact.CompilerVersion)
	wire, err := artifact.DeterministicJSON()
	require.NoError(t, err)
	require.NotContains(t, string(wire), `"feedbackScope"`)

	goldenPath := filepath.Join("testdata", "ir", "request_approval.json")
	golden, err := os.ReadFile(goldenPath)
	require.NoError(t, err)
	var compactGolden bytes.Buffer
	require.NoError(t, json.Compact(&compactGolden, golden))
	require.True(t, bytes.Equal(compactGolden.Bytes(), wire), "legacy request approval artifact bytes changed")
}

func TestRequestApprovalFeedbackScopeStateReadIsTracked(t *testing.T) {
	t.Parallel()

	source := strings.Replace(requestApprovalFeedbackScopePlan, "input:\n  feedback_scope: {type: scope, required: true}\n  request: {type: string, required: true}", "input:\n  request: {type: string, required: true}\nstate:\n  feedback_scope: {type: scope, required: true, default: {kind: channel, scopeId: chn_abcdefghijklmn}}", 1)
	source = strings.Replace(source, "      feedback_scope: {ref: plan.input.feedback_scope}\n      request:", "      feedback_scope: {ref: state.feedback_scope}\n      request:", 1)
	source = strings.Replace(source, "      feedback_scope: {type: scope, required: true}\n      feedback_scope: {type: scope, required: true}\n      request: {type: string, required: true}\n    requestApproval:", "      feedback_scope: {type: scope, required: true}\n      request: {type: string, required: true}\n    requestApproval:", 1)
	source = strings.Replace(source, "feedbackScope: {ref: input.feedback_scope}", "feedbackScope: {ref: state.feedback_scope}", 1)
	artifact := compileArtifactForTest(t, source)
	require.Equal(t, []string{"feedback_scope"}, artifactNodeForTest(artifact, "approve").StateEffects.Reads)
}

func mustDeterministicArtifactJSON(t *testing.T, artifact *Artifact) string {
	t.Helper()
	wire, err := artifact.DeterministicJSON()
	require.NoError(t, err)
	return string(wire)
}
