package declarative_test

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

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

	"github.com/agentregistry-dev/agentregistry/internal/cli/declarative"
	"github.com/agentregistry-dev/agentregistry/internal/client"
	arv0 "github.com/agentregistry-dev/agentregistry/pkg/api/v0"
)

// agentYAML is a minimal valid Agent document used across apply tests.
const agentYAML = `apiVersion: ar.dev/v1alpha1
kind: Agent
metadata:
  name: acme/bot
spec:
  image: ghcr.io/acme/bot:latest
  description: "A bot"
  language: python
  framework: adk
  modelProvider: google
  modelName: gemini-2.0-flash
`

// batchApplyResponse builds a JSON body matching the POST /v0/apply response shape.
func batchApplyResponse(results []arv0.ApplyResult) []byte {
	body, _ := json.Marshal(map[string]any{"results": results})
	return body
}

// newApplyTestServer creates an httptest.Server that records the last request and
// replies with the provided batch response. Returns the server and a pointer to the
// captured request (populated after the first HTTP call).
func newApplyTestServer(t *testing.T, results []arv0.ApplyResult) (*httptest.Server, *http.Request) {
	t.Helper()
	captured := &http.Request{}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		*captured = *r
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(batchApplyResponse(results))
	}))
	t.Cleanup(srv.Close)
	return srv, captured
}

// setupApplyClient wires a client pointing at srv into the declarative package.
func setupApplyClient(t *testing.T, srv *httptest.Server) {
	t.Helper()
	c := client.NewClient(srv.URL, "")
	declarative.SetAPIClient(c)
	t.Cleanup(func() { declarative.SetAPIClient(nil) })
}

// writeTempYAML writes content to a temp file and returns its path.
func writeTempYAML(t *testing.T, content string) string {
	t.Helper()
	dir := t.TempDir()
	p := filepath.Join(dir, "test.yaml")
	require.NoError(t, os.WriteFile(p, []byte(content), 0644))
	return p
}

// TestApplyPostsToBatchEndpoint verifies that apply sends POST to /v0/apply.
func TestApplyPostsToBatchEndpoint(t *testing.T) {
	results := []arv0.ApplyResult{
		{Kind: "agent", Name: "acme/bot", Tag: "1.0.0", Status: arv0.ApplyStatusConfigured},
	}
	srv, captured := newApplyTestServer(t, results)
	setupApplyClient(t, srv)

	var buf bytes.Buffer
	cmd := declarative.NewApplyCmd()
	cmd.SetOut(&buf)
	cmd.SetErr(&buf)
	cmd.SetArgs([]string{"-f", writeTempYAML(t, agentYAML)})
	require.NoError(t, cmd.Execute())

	assert.Equal(t, http.MethodPost, captured.Method)
	assert.Equal(t, "/v0/apply", captured.URL.Path)
}

// TestApplyPrintsPerResourceStatus verifies stdout contains per-resource lines.
func TestApplyPrintsPerResourceStatus(t *testing.T) {
	results := []arv0.ApplyResult{
		{Kind: "agent", Name: "a", Tag: "1.0", Status: arv0.ApplyStatusConfigured},
		{Kind: "deployment", Name: "x", Status: arv0.ApplyStatusFailed, Error: "drift detected"},
	}
	srv, _ := newApplyTestServer(t, results)
	setupApplyClient(t, srv)

	var out bytes.Buffer
	cmd := declarative.NewApplyCmd()
	cmd.SetOut(&out)
	cmd.SetErr(&out)
	cmd.SetArgs([]string{"-f", writeTempYAML(t, agentYAML)})
	err := cmd.Execute()
	// Expect non-zero because of the failed result.
	require.Error(t, err)

	output := out.String()
	assert.Contains(t, output, "✓ agent/a")
	assert.Contains(t, output, "✗ deployment/x")
}

// TestApplyReturnsErrorOnAnyFailure verifies a StatusFailed result causes non-zero exit.
func TestApplyReturnsErrorOnAnyFailure(t *testing.T) {
	results := []arv0.ApplyResult{
		{Kind: "skill", Name: "my-skill", Status: arv0.ApplyStatusFailed, Error: "internal error"},
	}
	srv, _ := newApplyTestServer(t, results)
	setupApplyClient(t, srv)

	cmd := declarative.NewApplyCmd()
	cmd.SetArgs([]string{"-f", writeTempYAML(t, agentYAML)})
	err := cmd.Execute()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "one or more resources failed")
}

// TestApplyDryRunFlag verifies --dry-run sets ?dryRun=true on the request.
func TestApplyDryRunFlag(t *testing.T) {
	results := []arv0.ApplyResult{
		{Kind: "agent", Name: "acme/bot", Tag: "1.0.0", Status: arv0.ApplyStatusDryRun},
	}
	srv, captured := newApplyTestServer(t, results)
	setupApplyClient(t, srv)

	var buf bytes.Buffer
	cmd := declarative.NewApplyCmd()
	cmd.SetOut(&buf)
	cmd.SetErr(&buf)
	cmd.SetArgs([]string{"-f", writeTempYAML(t, agentYAML), "--dry-run"})
	require.NoError(t, cmd.Execute())

	parsedQuery, err := url.ParseQuery(captured.URL.RawQuery)
	require.NoError(t, err)
	assert.Equal(t, "true", parsedQuery.Get("dryRun"), "expected ?dryRun=true in request URL")
}

// TestApplyNoQueryNoise verifies that omitting dry-run keeps the batch URL clean.
func TestApplyNoQueryNoise(t *testing.T) {
	results := []arv0.ApplyResult{
		{Kind: "agent", Name: "acme/bot", Status: arv0.ApplyStatusConfigured},
	}
	srv, captured := newApplyTestServer(t, results)
	setupApplyClient(t, srv)

	cmd := declarative.NewApplyCmd()
	cmd.SetArgs([]string{"-f", writeTempYAML(t, agentYAML)})
	require.NoError(t, cmd.Execute())
	assert.Empty(t, captured.URL.RawQuery, "expected no query params when dryRun is false")
}

// TestApplyRejectsUnknownKind verifies that an unknown kind fails before hitting the server.
func TestApplyRejectsUnknownKind(t *testing.T) {
	declarative.SetAPIClient(nil)
	defer declarative.SetAPIClient(nil)

	badYAML := `apiVersion: ar.dev/v1alpha1
kind: UnknownKind
metadata:
  name: acme/test
spec:
  description: "test"
`
	cmd := declarative.NewApplyCmd()
	cmd.SetArgs([]string{"-f", writeTempYAML(t, badYAML)})
	err := cmd.Execute()
	require.Error(t, err)
	assert.True(t, strings.Contains(err.Error(), "unknown resource type") ||
		strings.Contains(err.Error(), "UnknownKind"))
}

// TestApplyDryRunOutputAnnotated verifies that --dry-run output includes "(dry run)" suffix
// and reports the status returned by the server (created/configured).
func TestApplyDryRunOutputAnnotated(t *testing.T) {
	results := []arv0.ApplyResult{
		{Kind: "agent", Name: "acme/bot", Tag: "1.0.0", Status: arv0.ApplyStatusCreated},
		{Kind: "skill", Name: "my-skill", Tag: "2.0.0", Status: arv0.ApplyStatusConfigured},
	}
	srv, _ := newApplyTestServer(t, results)
	setupApplyClient(t, srv)

	var out bytes.Buffer
	cmd := declarative.NewApplyCmd()
	cmd.SetOut(&out)
	cmd.SetErr(&out)
	cmd.SetArgs([]string{"-f", writeTempYAML(t, agentYAML), "--dry-run"})
	require.NoError(t, cmd.Execute())

	output := out.String()
	assert.Contains(t, output, "agent/acme/bot")
	assert.Contains(t, output, "created")
	assert.Contains(t, output, "(dry run)")
	assert.Contains(t, output, "skill/my-skill")
	assert.Contains(t, output, "configured")
}

// TestApplyNoAPIClient verifies that a missing API client returns an error.
func TestApplyNoAPIClient(t *testing.T) {
	declarative.SetAPIClient(nil)
	defer declarative.SetAPIClient(nil)

	cmd := declarative.NewApplyCmd()
	cmd.SetArgs([]string{"-f", writeTempYAML(t, agentYAML)})
	err := cmd.Execute()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "API client not initialized")
}

// TestApply_InjectsLabelsFromArctlYAML verifies that injectArctlLabels reads a
// sibling arctl.yaml and merges arctl.dev/framework + arctl.dev/language into
// metadata.labels on the envelope.
func TestApply_InjectsLabelsFromArctlYAML(t *testing.T) {
	tmp := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(tmp, "arctl.yaml"),
		[]byte("framework: adk\nlanguage: python\n"), 0644))
	require.NoError(t, os.WriteFile(filepath.Join(tmp, "agent.yaml"), []byte(`apiVersion: ar.dev/v1alpha1
kind: Agent
metadata:
  name: foo
  version: "1"
spec:
  title: Foo
`), 0644))

	got, err := declarative.InjectArctlLabels(filepath.Join(tmp, "agent.yaml"))
	require.NoError(t, err)
	assert.Contains(t, string(got), "arctl.dev/framework: adk")
	assert.Contains(t, string(got), "arctl.dev/language: python")
}

// TestApply_InjectArctlLabels_PassThroughWithoutArctlYAML verifies that when no
// sibling arctl.yaml exists, the original bytes are returned unchanged.
func TestApply_InjectArctlLabels_PassThroughWithoutArctlYAML(t *testing.T) {
	tmp := t.TempDir()
	original := []byte(`apiVersion: ar.dev/v1alpha1
kind: Agent
metadata:
  name: foo
  version: "1"
spec:
  title: Foo
`)
	require.NoError(t, os.WriteFile(filepath.Join(tmp, "agent.yaml"), original, 0644))

	got, err := declarative.InjectArctlLabels(filepath.Join(tmp, "agent.yaml"))
	require.NoError(t, err)
	assert.Equal(t, string(original), string(got))
}

// TestApply_InjectArctlLabels_SkipsNonAgentKinds verifies that injection is
// limited to Agent and MCPServer kinds (skill, prompt, etc. are pass-through).
func TestApply_InjectArctlLabels_SkipsNonAgentKinds(t *testing.T) {
	tmp := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(tmp, "arctl.yaml"),
		[]byte("framework: adk\nlanguage: python\n"), 0644))
	skillYAML := `apiVersion: ar.dev/v1alpha1
kind: Skill
metadata:
  name: foo
  version: "1"
spec:
  title: Foo
`
	require.NoError(t, os.WriteFile(filepath.Join(tmp, "skill.yaml"), []byte(skillYAML), 0644))

	got, err := declarative.InjectArctlLabels(filepath.Join(tmp, "skill.yaml"))
	require.NoError(t, err)
	assert.NotContains(t, string(got), "arctl.dev/framework")
	assert.NotContains(t, string(got), "arctl.dev/language")
}

// TestApply_InjectArctlLabels_MCPServer verifies that MCPServer kind also gets
// labels injected.
func TestApply_InjectArctlLabels_MCPServer(t *testing.T) {
	tmp := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(tmp, "arctl.yaml"),
		[]byte("framework: fastmcp\nlanguage: python\n"), 0644))
	mcpYAML := `apiVersion: ar.dev/v1alpha1
kind: MCPServer
metadata:
  name: foo
  version: "1"
spec:
  description: A server
`
	require.NoError(t, os.WriteFile(filepath.Join(tmp, "mcp.yaml"), []byte(mcpYAML), 0644))

	got, err := declarative.InjectArctlLabels(filepath.Join(tmp, "mcp.yaml"))
	require.NoError(t, err)
	assert.Contains(t, string(got), "arctl.dev/framework: fastmcp")
	assert.Contains(t, string(got), "arctl.dev/language: python")
}
