package forwarder

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/agent"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
)

func TestSkillGatewayForwardsFiveOperationsWithResolvedHumanIdentity(t *testing.T) {
	var requests []string
	var bodies []string
	var claims []map[string]any
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		requests = append(requests, request.Method+" "+request.URL.RequestURI())
		require.Equal(t, "wsp_1", request.Header.Get("X-Workspace-Id"))
		claim := decodeClaims(t, strings.TrimPrefix(request.Header.Get("Authorization"), "Bearer "))
		claims = append(claims, claim)
		require.NotContains(t, claim, "owned_agent_ids")
		require.NotContains(t, claim, "agent_ownership_workspace_id")

		switch {
		case request.Method == http.MethodGet && request.URL.Path == "/api/v1/skills":
			_, _ = io.WriteString(writer, `{"items":[],"page":2,"perPage":7,"total":0,"hasMore":false}`)
		case request.Method == http.MethodGet:
			_, _ = io.WriteString(writer, `{"id":"skill_1","name":"Release","repositories":{}}`)
		case request.Method == http.MethodPost && request.URL.Path == "/api/v1/skills":
			body, err := io.ReadAll(request.Body)
			require.NoError(t, err)
			bodies = append(bodies, string(body))
			writer.WriteHeader(http.StatusCreated)
			_, _ = io.WriteString(writer, `{"id":"skill_1","name":"Release","repositories":{}}`)
		case request.Method == http.MethodPatch:
			body, err := io.ReadAll(request.Body)
			require.NoError(t, err)
			bodies = append(bodies, string(body))
			_, _ = io.WriteString(writer, `{"id":"skill_1","name":"Renamed","repositories":{}}`)
		case request.Method == http.MethodPost && strings.HasSuffix(request.URL.Path, "/imports"):
			require.Equal(t, int64(-1), request.ContentLength)
			require.Equal(t, []string{"chunked"}, request.TransferEncoding)
			require.NoError(t, request.ParseMultipartForm(1024))
			require.Equal(t, "https://example.test/skills.zip", request.FormValue("url"))
			_, _ = io.WriteString(writer, `{"skill":{"id":"skill_1","name":"Release","repositories":{}},"importedSkills":[],"skillCount":1}`)
		default:
			t.Fatalf("unexpected upstream request %s %s", request.Method, request.URL.RequestURI())
		}
	}))
	t.Cleanup(upstream.Close)

	gateway, err := NewSkillGateway(Dependencies{
		BaseURL: upstream.URL, HTTPClient: upstream.Client(),
	})
	require.NoError(t, err)
	principal := agent.AgentSkillPrincipal{WorkspaceID: "wsp_1", Subject: "usr_human"}

	_, err = gateway.List(context.Background(), principal, agent.AgentSkillListInput{
		Query: "release", Page: 2, PerPage: 7,
		TemplateLocale: "en-US", View: "created_by_me",
	})
	require.NoError(t, err)
	_, err = gateway.Get(context.Background(), principal, "skill_1")
	require.NoError(t, err)
	active := true
	_, err = gateway.Create(context.Background(), principal, dto.SkillCreateRequest{
		Name: "Release", Description: "Release helper", IsActive: &active,
	})
	require.NoError(t, err)
	empty := ""
	_, err = gateway.Update(context.Background(), principal, "skill_1", agent.AgentSkillUpdateInput{
		Description: &empty,
	})
	require.NoError(t, err)

	reader, writer := io.Pipe()
	multipartWriter := multipart.NewWriter(writer)
	contentType := multipartWriter.FormDataContentType()
	go func() {
		writeErr := multipartWriter.WriteField("url", "https://example.test/skills.zip")
		if writeErr == nil {
			writeErr = multipartWriter.Close()
		}
		if writeErr != nil {
			_ = writer.CloseWithError(writeErr)
		} else {
			_ = writer.Close()
		}
	}()
	_, err = gateway.Import(context.Background(), principal, "skill_1", contentType, reader)
	require.NoError(t, err)

	require.Equal(t, []string{
		"GET /api/v1/skills?page=2&per_page=7&q=release&template_locale=en-US&view=created_by_me",
		"GET /api/v1/skills/skill_1",
		"POST /api/v1/skills",
		"PATCH /api/v1/skills/skill_1",
		"POST /api/v1/skills/skill_1/imports",
	}, requests)
	require.JSONEq(t, `{"name":"Release","description":"Release helper","isActive":true}`, bodies[0])
	require.JSONEq(t, `{"description":""}`, bodies[1])
	require.Len(t, claims, 5)
	for _, claim := range claims {
		require.Equal(t, "usr_human", claim["sub"])
		require.Equal(t, "usr_human", claim["agentteam_user_id"])
	}
}

func TestSkillGatewayImportDoesNotInheritJSONTotalTimeout(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		require.NoError(t, request.ParseMultipartForm(1024))
		require.Equal(t, "before", request.FormValue("first"))
		require.Equal(t, "after", request.FormValue("second"))
		_, _ = io.WriteString(writer, `{"skill":{"id":"skill_1","name":"Release","repositories":{}},"importedSkills":[],"skillCount":1}`)
	}))
	t.Cleanup(upstream.Close)

	jsonClient := upstream.Client()
	jsonClient.Timeout = 10 * time.Millisecond
	gateway, err := NewSkillGateway(Dependencies{
		BaseURL: upstream.URL, HTTPClient: jsonClient,
	})
	require.NoError(t, err)
	gateway.upstream.streamingHTTPClient = newMultipartStreamingHTTPClient(
		jsonClient, 15*time.Millisecond,
	)

	reader, pipeWriter := io.Pipe()
	writer := multipart.NewWriter(pipeWriter)
	contentType := writer.FormDataContentType()
	go func() {
		writeErr := writer.WriteField("first", "before")
		if writeErr == nil {
			uploadDelay := time.NewTimer(50 * time.Millisecond)
			defer uploadDelay.Stop()
			<-uploadDelay.C
			writeErr = writer.WriteField("second", "after")
		}
		if writeErr == nil {
			writeErr = writer.Close()
		}
		if writeErr != nil {
			_ = pipeWriter.CloseWithError(writeErr)
		} else {
			_ = pipeWriter.Close()
		}
	}()

	result, err := gateway.Import(
		context.Background(),
		agent.AgentSkillPrincipal{WorkspaceID: "wsp_1", Subject: "usr_human"},
		"skill_1",
		contentType,
		reader,
	)

	require.NoError(t, err)
	require.Equal(t, "skill_1", result.Skill.ID)
	require.Equal(t, 10*time.Millisecond, jsonClient.Timeout)
	require.Zero(t, gateway.upstream.streamingHTTPClient.Timeout)
	streamingTransport, ok := gateway.upstream.streamingHTTPClient.Transport.(*http.Transport)
	require.True(t, ok)
	require.Equal(t, 15*time.Millisecond, streamingTransport.ResponseHeaderTimeout)
}

func TestSkillGatewayImportTimesOutWaitingForHeadersAfterBodyEOF(t *testing.T) {
	bodyRead := make(chan struct{})
	lateHeader := make(chan struct{})
	releaseHeader := make(chan struct{})
	defer close(releaseHeader)
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		_, _ = io.Copy(io.Discard, request.Body)
		close(bodyRead)
		<-releaseHeader
		close(lateHeader)
		_, _ = io.WriteString(writer, `{"skill":{"id":"skill_1","name":"Release","repositories":{}},"importedSkills":[],"skillCount":1}`)
	}))
	t.Cleanup(upstream.Close)

	jsonClient := upstream.Client()
	jsonClient.Timeout = 10 * time.Millisecond
	gateway, err := NewSkillGateway(Dependencies{
		BaseURL: upstream.URL, HTTPClient: jsonClient,
	})
	require.NoError(t, err)
	gateway.upstream.streamingHTTPClient = newMultipartStreamingHTTPClient(
		jsonClient, 25*time.Millisecond,
	)

	reader, pipeWriter := io.Pipe()
	writer := multipart.NewWriter(pipeWriter)
	contentType := writer.FormDataContentType()
	go func() {
		writeErr := writer.WriteField("field", "value")
		if writeErr == nil {
			writeErr = writer.Close()
		}
		if writeErr != nil {
			_ = pipeWriter.CloseWithError(writeErr)
		} else {
			_ = pipeWriter.Close()
		}
	}()

	_, err = gateway.Import(
		context.Background(),
		agent.AgentSkillPrincipal{WorkspaceID: "wsp_1", Subject: "usr_human"},
		"skill_1",
		contentType,
		reader,
	)

	require.Error(t, err)
	select {
	case <-bodyRead:
	default:
		t.Fatal("upstream must consume the complete request body before the header timeout starts")
	}
	select {
	case <-lateHeader:
		t.Fatal("gateway must fail before the delayed upstream header is written")
	default:
	}
}

func TestSkillGatewayRejectsUnsafeSkillPathSegmentsBeforeUpstream(t *testing.T) {
	var calls int
	upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		calls++
	}))
	t.Cleanup(upstream.Close)
	gateway, err := NewSkillGateway(Dependencies{
		BaseURL: upstream.URL, HTTPClient: upstream.Client(),
	})
	require.NoError(t, err)
	principal := agent.AgentSkillPrincipal{WorkspaceID: "wsp_1", Subject: "usr_human"}

	for _, skillID := range []string{
		"", ".", "..", "skill/child", "skill?view=all", "skill#fragment",
		"skill%2fchild", "skill%3fview=all",
	} {
		_, getErr := gateway.Get(context.Background(), principal, skillID)
		require.Error(t, getErr, skillID)
	}
	require.Zero(t, calls)
}

func TestSkillGatewayMapsUpstreamFailureToSafeHTTPError(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.WriteHeader(http.StatusForbidden)
		_ = json.NewEncoder(writer).Encode(map[string]string{
			"code": "skill_forbidden", "message": "sensitive upstream reason",
		})
	}))
	t.Cleanup(upstream.Close)
	gateway, err := NewSkillGateway(Dependencies{
		BaseURL: upstream.URL, HTTPClient: upstream.Client(),
	})
	require.NoError(t, err)
	_, err = gateway.Get(context.Background(), agent.AgentSkillPrincipal{
		WorkspaceID: "wsp_1", Subject: "usr_human",
	}, "skill_1")
	require.Error(t, err)
	require.NotContains(t, err.Error(), "sensitive upstream reason")
	require.Equal(t, http.StatusForbidden, agentSkillHTTPStatus(t, err))
}

func TestSkillGatewayDeleteUsesExistingResourceRouteAndMaps404(t *testing.T) {
	var requests []string
	upstream := httptest.NewServer(http.HandlerFunc(func(
		writer http.ResponseWriter,
		request *http.Request,
	) {
		requests = append(requests, request.Method+" "+request.URL.Path)
		claims := decodeClaims(
			t, strings.TrimPrefix(request.Header.Get("Authorization"), "Bearer "),
		)
		require.Equal(t, "usr_human", claims["sub"])
		require.Equal(t, "wsp_1", request.Header.Get("X-Workspace-Id"))
		if strings.HasSuffix(request.URL.Path, "/missing") {
			writer.WriteHeader(http.StatusNotFound)
			_, _ = io.WriteString(writer, `{"code":"skill_not_found"}`)
			return
		}
		writer.WriteHeader(http.StatusNoContent)
	}))
	t.Cleanup(upstream.Close)

	gateway, err := NewSkillGateway(Dependencies{
		BaseURL: upstream.URL, HTTPClient: upstream.Client(),
	})
	require.NoError(t, err)
	principal := agent.AgentSkillPrincipal{
		WorkspaceID: "wsp_1", Subject: "usr_human",
	}

	require.NoError(t, gateway.Delete(
		context.Background(), principal, "skill_1",
	))
	err = gateway.Delete(context.Background(), principal, "missing")
	require.ErrorIs(t, err, api.ErrNotFound)
	require.Equal(t, []string{
		"DELETE /api/v1/skills/skill_1",
		"DELETE /api/v1/skills/missing",
	}, requests)
}

func agentSkillHTTPStatus(t *testing.T, err error) int {
	t.Helper()
	var httpErr api.HTTPError
	require.True(t, errors.As(err, &httpErr))
	return httpErr.Status
}
