package skill

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/desktop/internal/provisioning/asset"
)

// accessBatchRequest is the test-local minimal view of the access-batch request
// body the provider posts; tests decode into it to assert the agent id.
type accessBatchRequest struct {
	AgentIDs []string `json:"agentIds"`
}

// stubResolver is a ServerConnectionResolver stub that always returns the canned
// connection info. Tests use it to point the provider at the httptest server.
type stubResolver struct {
	conn asset.ConnectionInfo
}

func (s *stubResolver) ResolveServerForAgent(_ context.Context, _ string) (asset.ConnectionInfo, bool) {
	return s.conn, true
}

func TestProvider_LookupPostsToSkillsBatchAccessEndpoint(t *testing.T) {
	var capturedPath, capturedMethod, capturedAuth, capturedWorkspace, capturedRouteWorkspace, capturedBody string
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		capturedPath = r.URL.Path
		capturedMethod = r.Method
		capturedAuth = r.Header.Get("Authorization")
		capturedWorkspace = r.Header.Get("X-Workspace-Id")
		capturedRouteWorkspace = r.Header.Get("X-AgentTeam-Route-Workspace-Id")
		if body, err := io.ReadAll(r.Body); err == nil {
			capturedBody = string(body)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"agents":{"agt_xxx":[{"id":"skill_1","name":"Backend Skills","branch":"main","repository":{"repoId":"sk_abc","repoOwner":"agentteam","repoName":"ws-1.sk","repoFullName":"agentteam/ws-1.sk"}}]},"syncToken":"ags_rat_skill","syncTokenExpires":"2099-01-02T03:04:05Z"}`))
	}))
	defer ts.Close()

	resolver := &stubResolver{conn: asset.ConnectionInfo{
		ServerURL:      ts.URL,
		UserToken:      "tok_abc",
		WorkspaceID:    "ws_1",
		TemplateLocale: "zh-CN",
	}}
	provider := New(resolver)

	access, state, err := provider.Lookup(context.Background(), "agt_xxx")
	require.NoError(t, err)
	require.Equal(t, LookupBound, state, "server reports bindings; state must be Bound")
	require.Len(t, access, 1)

	// Endpoint MUST be under the canonical workspace skills path — never /repos.
	require.Equal(t, "/api/v1/workspaces/ws_1/skills/agents/access:batch", capturedPath)
	require.NotContains(t, capturedPath, "/repos")
	require.Equal(t, http.MethodPost, capturedMethod)
	require.Equal(t, "Bearer tok_abc", capturedAuth)
	require.Equal(t, "ws_1", capturedWorkspace)
	require.Equal(t, capturedWorkspace, capturedRouteWorkspace)
	require.JSONEq(t, `{"agentIds":["agt_xxx"],"templateLocale":"zh-CN"}`, capturedBody)

	got := access[0]
	require.Equal(t, "skill_1", got.SkillID)
	require.Equal(t, "Backend Skills", got.Name)
	require.Equal(t, "main", got.Branch)
	require.Equal(t, "sk_abc", got.RepoID)
	require.Equal(t, ts.URL+"/api/v1/workspaces/ws_1/ags/agentteam/ws-1.sk.git", got.CloneURL)
	req, err := http.NewRequest(http.MethodGet, got.CloneURL+"/info/refs", nil)
	require.NoError(t, err)
	got.Auth.SetAuth(req)
	require.Equal(t, "Bearer ags_rat_skill", req.Header.Get("Authorization"))
}

// TestProvider_LookupSkipsRepoRefWithoutFullName now expects LookupUnbound:
// the server reached an authoritative answer, but after filtering invalid
// refs the agent has zero accessible skills. That is an authoritative empty
// result, NOT Unknown — callers are allowed to clear caches.
func TestProvider_LookupSkipsRepoRefWithoutFullName(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(batchResponse{
			Agents: map[string][]*accessibleSkill{
				"agt_xxx": {
					{
						ID:     "skill_1",
						Branch: "main",
						Repository: &repositoryRef{
							RepoID:    "sk_abc",
							RepoOwner: "agentteam",
							RepoName:  "backup.sk",
						},
					},
				},
			},
		})
	}))
	defer ts.Close()

	provider := New(&stubResolver{conn: asset.ConnectionInfo{
		ServerURL: ts.URL, UserToken: "tok", WorkspaceID: "ws",
	}})
	access, state, err := provider.Lookup(context.Background(), "agt_xxx")
	require.NoError(t, err)
	require.Equal(t, LookupUnbound, state, "server reached; no valid skills means authoritative empty")
	require.Empty(t, access)
}

func TestProvider_LookupSkipsSkillWithIncompleteRepoRef(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(batchResponse{
			Agents: map[string][]*accessibleSkill{
				"agt_xxx": {
					// Missing Repository entirely.
					{ID: "skill_1", Branch: "main"},
					// Missing both RepoFullName and Owner/Name.
					{ID: "skill_2", Branch: "main", Repository: &repositoryRef{RepoID: "sk_2"}},
					// Missing branch.
					{
						ID:         "skill_3",
						Repository: &repositoryRef{RepoID: "sk_3", RepoFullName: "agentteam/x.sk"},
					},
					// Valid.
					{
						ID:     "skill_4",
						Branch: "main",
						Repository: &repositoryRef{
							RepoID: "sk_4", RepoFullName: "agentteam/valid.sk",
						},
					},
				},
			},
			SyncToken:        "ags_rat_skill",
			SyncTokenExpires: "2099-01-02T03:04:05Z",
		})
	}))
	defer ts.Close()

	provider := New(&stubResolver{conn: asset.ConnectionInfo{
		ServerURL: ts.URL, UserToken: "tok", WorkspaceID: "ws",
	}})
	access, state, err := provider.Lookup(context.Background(), "agt_xxx")
	require.NoError(t, err)
	require.Equal(t, LookupBound, state, "one valid skill survived filtering; state must be Bound")
	require.Len(t, access, 1, "only the valid entry should survive")
	require.Equal(t, "skill_4", access[0].SkillID)
}

func TestProvider_LookupReturnsUnknownWhenBoundResponseHasNoSyncLease(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(batchResponse{
			Agents: map[string][]*accessibleSkill{
				"agt_xxx": {
					{
						ID:     "skill_1",
						Branch: "main",
						Repository: &repositoryRef{
							RepoID: "sk_1", RepoFullName: "agentteam/valid.sk",
						},
					},
				},
			},
		})
	}))
	defer ts.Close()

	provider := New(&stubResolver{conn: asset.ConnectionInfo{
		ServerURL: ts.URL, UserToken: "tok", WorkspaceID: "ws",
	}})
	access, state, err := provider.Lookup(context.Background(), "agt_xxx")
	require.Error(t, err)
	require.Equal(t, LookupUnknown, state)
	require.Nil(t, access)
}

func TestProvider_LookupReturnsEmptyWhenAgentHasNoSkills(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(batchResponse{Agents: map[string][]*accessibleSkill{"agt_xxx": {}}})
	}))
	defer ts.Close()

	provider := New(&stubResolver{conn: asset.ConnectionInfo{
		ServerURL: ts.URL, UserToken: "tok", WorkspaceID: "ws",
	}})
	access, state, err := provider.Lookup(context.Background(), "agt_xxx")
	require.NoError(t, err)
	require.Equal(t, LookupUnbound, state, "server reached + agent has no skills is authoritative empty")
	require.Empty(t, access)
}

func TestProvider_LookupReturnsUnknownWhenAgentOmittedFromResponse(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(batchResponse{Agents: map[string][]*accessibleSkill{"agt_other": {}}})
	}))
	defer ts.Close()

	provider := New(&stubResolver{conn: asset.ConnectionInfo{
		ServerURL: ts.URL, UserToken: "tok", WorkspaceID: "ws",
	}})
	access, state, err := provider.Lookup(context.Background(), "agt_xxx")
	require.Error(t, err)
	require.Equal(t, LookupUnknown, state, "omitted agent means Desktop observed an incomplete response, not authoritative unbound")
	require.Nil(t, access)
}

// TestProvider_LookupReturnsUnknownOnNonOKStatus pins the three-state
// contract for HTTP failures: a non-200 response is a transient resolver
// failure, NOT an authoritative revocation. State must be Unknown so callers
// preserve cached bindings.
func TestProvider_LookupReturnsUnknownOnNonOKStatus(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
		_, _ = w.Write([]byte(`{"error":"unauthorized"}`))
	}))
	defer ts.Close()

	provider := New(&stubResolver{conn: asset.ConnectionInfo{
		ServerURL: ts.URL, UserToken: "tok", WorkspaceID: "ws",
	}})
	access, state, err := provider.Lookup(context.Background(), "agt_xxx")
	require.Error(t, err)
	require.Contains(t, err.Error(), "401")
	require.Equal(t, LookupUnknown, state, "HTTP failure is transient; state must preserve caller caches")
	require.Nil(t, access)
}

// TestProvider_LookupReturnsUnknownWhenChannelUnresolved pins the contract
// for resolver failures: a missing channel or token is a transient resolver
// failure, NOT an authoritative revocation. State must be Unknown.
func TestProvider_LookupReturnsUnknownWhenChannelUnresolved(t *testing.T) {
	provider := New(&stubResolver{conn: asset.ConnectionInfo{}})
	access, state, err := provider.Lookup(context.Background(), "agt_xxx")
	require.Error(t, err)
	require.Equal(t, LookupUnknown, state, "resolver failure must NOT be treated as authoritative revocation")
	require.Nil(t, access)
}

func TestProvider_LookupTrimsAgentID(t *testing.T) {
	called := false
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true
		var req accessBatchRequest
		_ = json.NewDecoder(r.Body).Decode(&req)
		require.Equal(t, "agt_xxx", req.AgentIDs[0])
		_ = json.NewEncoder(w).Encode(batchResponse{Agents: map[string][]*accessibleSkill{"agt_xxx": {}}})
	}))
	defer ts.Close()

	provider := New(&stubResolver{conn: asset.ConnectionInfo{
		ServerURL: ts.URL, UserToken: "tok", WorkspaceID: "ws",
	}})
	_, state, err := provider.Lookup(context.Background(), "  agt_xxx  ")
	require.NoError(t, err)
	require.True(t, called)
	require.Equal(t, LookupUnbound, state)
}

func TestComposeCloneURLRequiresRepoFullName(t *testing.T) {
	// No repository ref at all.
	require.Equal(t, "", composeCloneURL("https://x", "wsp_1", nil))
	// Empty ref.
	require.Equal(t, "", composeCloneURL("https://x", "wsp_1", &repositoryRef{}))
	// Full name wins.
	require.Equal(t,
		"https://x/api/v1/workspaces/wsp_1/ags/owner/repo.sk.git",
		composeCloneURL("https://x", "wsp_1", &repositoryRef{
			RepoOwner: "ignored", RepoName: "ignored",
			RepoFullName: "owner/repo.sk",
		}))
	// Owner/name without repoFullName is intentionally rejected.
	require.Equal(t, "",
		composeCloneURL("https://x", "wsp_1", &repositoryRef{
			RepoOwner: "owner", RepoName: "repo.sk",
		}))
	// Half owner/name also fails.
	require.Equal(t, "",
		composeCloneURL("https://x", "wsp_1", &repositoryRef{RepoOwner: "owner"}))
}

// TestProvider_LookupEndpointHasNoReposPathSegment is a belt-and-braces
// guard: the batch access endpoint is a skills-only contract per the MeshX
// server-side hard constraint. This catches accidental regressions that route
// through the older /repos path.
func TestProvider_LookupEndpointHasNoReposPathSegment(t *testing.T) {
	var paths []string
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		paths = append(paths, r.URL.Path)
		_ = json.NewEncoder(w).Encode(batchResponse{Agents: map[string][]*accessibleSkill{"agt_xxx": {}}})
	}))
	defer ts.Close()

	provider := New(&stubResolver{conn: asset.ConnectionInfo{
		ServerURL: ts.URL, UserToken: "tok", WorkspaceID: "ws",
	}})
	_, state, err := provider.Lookup(context.Background(), "agt_xxx")
	require.NoError(t, err)
	require.Equal(t, LookupUnbound, state)
	require.NotEmpty(t, paths)
	for _, p := range paths {
		require.False(t, strings.Contains(p, "/repos"),
			"endpoint path %q must not include /repos", p)
	}
}
