package server

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	agsconfig "github.com/pingcap/agent-git-service/config"
	agsserver "github.com/pingcap/agent-git-service/server"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/service/automation"
)

type automationRunnerTestRun struct {
	ID          string `json:"id"`
	CommitSHA   string `json:"commitSha"`
	TriggerKind string `json:"triggerKind"`
	TriggerRef  string `json:"triggerRef"`
}

type automationRunnerAcquiredJob struct {
	QueueJobID         int64    `json:"queueJobId"`
	JobRunID           string   `json:"jobRunId"`
	JobID              string   `json:"jobId"`
	LeaseOwner         string   `json:"leaseOwner"`
	RepositoryFullName string   `json:"repositoryFullName"`
	CommitSHA          string   `json:"commitSha"`
	RunsOnJSON         string   `json:"runsOnJson"`
	Labels             []string `json:"labels"`
	InputJSON          string   `json:"inputJson"`
	TriggerContextJSON string   `json:"triggerContextJson"`
}

type automationRunnerRegistration struct {
	RunnerID          string `json:"runnerId"`
	RegistrationToken string `json:"registrationToken"`
}

type automationRunnerSession struct {
	SessionID    string `json:"sessionId"`
	LeaseOwner   string `json:"leaseOwner"`
	SessionToken string `json:"sessionToken"`
}

type automationLogChunk struct {
	ID        string `json:"id"`
	JobRunID  string `json:"jobRunId"`
	Stream    string `json:"stream"`
	Sequence  int64  `json:"sequence"`
	SizeBytes int64  `json:"sizeBytes"`
}

type automationLogChunkContent struct {
	automationLogChunk
	Content string `json:"content"`
}

type automationArtifact struct {
	ID        string `json:"id"`
	JobRunID  string `json:"jobRunId"`
	Name      string `json:"name"`
	SizeBytes int64  `json:"sizeBytes"`
}

type automationArtifactContent struct {
	automationArtifact
	ContentBase64 string `json:"contentBase64"`
}

func newAutomationRunnerTestServer(t *testing.T, automationYAML string, commitSHA string) *Server {
	t.Helper()
	return newAutomationRunnerTestServerWithAutomationConfig(t, automationYAML, commitSHA, config.AutomationConfig{})
}

func newAutomationRunnerTestServerWithAutomationConfig(t *testing.T, automationYAML string, commitSHA string, automationConfig config.AutomationConfig) *Server {
	t.Helper()
	srv, err := New(config.ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        config.ResourceRuntimeAuthConfig{Enabled: true, Header: "Authorization"},
		Database:    config.ResourceDatabaseConfig{DSN: meshxDatabaseDSN(t)},
		AGS:         config.AGSConfig{DatabaseDSN: prepareResolverDB(t), GitRepoDir: t.TempDir(), BaseURL: "http://" + actionsRunnerHost},
		Automation:  automationConfig,
	}, Options{AutomationAuthorizer: automation.AllowAutomationAuthorizer{}, NewAGS: func(agsconfig.Config, ...agsserver.Option) (agsServer, error) {
		agsHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch {
			case r.URL.Path == "/api/v3/repos/pingcap/agentteam/git/refs/heads/main":
				_ = json.NewEncoder(w).Encode(map[string]any{"ref": "refs/heads/main", "object": map[string]any{"sha": commitSHA, "type": "commit"}})
			case r.URL.Path == "/api/v3/repos/pingcap/agentteam/commits/"+commitSHA:
				_ = json.NewEncoder(w).Encode(map[string]any{"sha": commitSHA})
			case r.URL.Path == "/api/v3/repos/pingcap/agentteam/contents/.agentteam/runner.yml" && r.URL.Query().Get("ref") == commitSHA:
				_ = json.NewEncoder(w).Encode(map[string]any{"type": "file", "encoding": "base64", "content": base64.StdEncoding.EncodeToString([]byte(automationYAML))})
			default:
				http.NotFound(w, r)
			}
		})
		return fakeAGS{handler: agsHandler}, nil
	}})
	require.NoError(t, err)
	installLegacyAutomationSourceForTest(srv)
	t.Cleanup(func() { require.NoError(t, srv.Shutdown(context.Background())) })
	return srv
}

func newAutomationRunnerMutableRefTestServer(t *testing.T, automationYAML string, branchCommit *string, commits ...string) *Server {
	t.Helper()
	allowedCommits := make(map[string]struct{}, len(commits))
	for _, commit := range commits {
		allowedCommits[commit] = struct{}{}
	}
	srv, err := New(config.ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        config.ResourceRuntimeAuthConfig{Enabled: true, Header: "Authorization"},
		Database:    config.ResourceDatabaseConfig{DSN: meshxDatabaseDSN(t)},
		AGS:         config.AGSConfig{DatabaseDSN: prepareResolverDB(t), GitRepoDir: t.TempDir()},
	}, Options{AutomationAuthorizer: automation.AllowAutomationAuthorizer{}, NewAGS: func(agsconfig.Config, ...agsserver.Option) (agsServer, error) {
		agsHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			current := ""
			if branchCommit != nil {
				current = *branchCommit
			}
			switch {
			case r.URL.Path == "/api/v3/repos/pingcap/agentteam/git/refs/heads/main":
				_ = json.NewEncoder(w).Encode(map[string]any{"ref": "refs/heads/main", "object": map[string]any{"sha": current, "type": "commit"}})
			case strings.HasPrefix(r.URL.Path, "/api/v3/repos/pingcap/agentteam/commits/"):
				commit := strings.TrimPrefix(r.URL.Path, "/api/v3/repos/pingcap/agentteam/commits/")
				if _, ok := allowedCommits[commit]; ok {
					_ = json.NewEncoder(w).Encode(map[string]any{"sha": commit})
					return
				}
				http.NotFound(w, r)
			case r.URL.Path == "/api/v3/repos/pingcap/agentteam/contents/.agentteam/runner.yml":
				if _, ok := allowedCommits[r.URL.Query().Get("ref")]; ok {
					_ = json.NewEncoder(w).Encode(map[string]any{"type": "file", "encoding": "base64", "content": base64.StdEncoding.EncodeToString([]byte(automationYAML))})
					return
				}
				http.NotFound(w, r)
			default:
				http.NotFound(w, r)
			}
		})
		return fakeAGS{handler: agsHandler}, nil
	}})
	require.NoError(t, err)
	installLegacyAutomationSourceForTest(srv)
	t.Cleanup(func() { require.NoError(t, srv.Shutdown(context.Background())) })
	return srv
}

type availableAutomationRepositoryCredential struct{}

func (availableAutomationRepositoryCredential) CheckAutomationRepositoryCredential(context.Context, automation.AutomationRepositoryCredentialCheckRequest) (automation.AutomationRepositoryCredentialAvailability, error) {
	return automation.AutomationRepositoryCredentialAvailability{RepoTokenAvailable: true, RepoTokenScope: "read_only"}, nil
}

func installLegacyAutomationSourceForTest(srv *Server) {
	if srv == nil || srv.automations == nil || srv.ags == nil {
		return
	}
	srv.automations.SetSourceResolver(automation.AGSHTTPAutomationSourceResolver{
		Handler: srv.ags.Handler(), AuthHeader: "Authorization",
	})
	srv.automations.SetRepositoryCredentialChecker(availableAutomationRepositoryCredential{})
}

func startAutomationRunnerTestRun(t *testing.T, srv *Server, auth string, workspaceID string, definitionPath string) automationRunnerTestRun {
	t.Helper()
	body, err := json.Marshal(map[string]string{"ref": "refs/heads/main", "definitionPath": definitionPath})
	require.NoError(t, err)
	startReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/repositories/pingcap/agentteam/runs", bytes.NewBuffer(body))
	setAutomationRunnerHeaders(startReq, auth, workspaceID)
	startRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(startRec, startReq)
	require.Equal(t, http.StatusCreated, startRec.Code, startRec.Body.String())
	var run automationRunnerTestRun
	require.NoError(t, json.Unmarshal(startRec.Body.Bytes(), &run))
	require.NotEmpty(t, run.ID)
	require.False(t, srv.meshxDB.Migrator().HasTable("automation_definitions"))
	require.False(t, srv.meshxDB.Migrator().HasTable("automation_versions"))
	return run
}

func registerAutomationRunnerSession(t *testing.T, srv *Server, humanAuth string, workspaceID string, runnerID string, sessionID string, labels []string) string {
	t.Helper()
	registerBody, err := json.Marshal(map[string]any{"runnerId": runnerID, "name": runnerID, "labels": labels})
	require.NoError(t, err)
	registerReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/runners/register", bytes.NewBuffer(registerBody))
	setAutomationRunnerHeaders(registerReq, humanAuth, workspaceID)
	registerRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(registerRec, registerReq)
	require.Equal(t, http.StatusCreated, registerRec.Code, registerRec.Body.String())
	var registration automationRunnerRegistration
	require.NoError(t, json.Unmarshal(registerRec.Body.Bytes(), &registration))
	require.Equal(t, runnerID, registration.RunnerID)
	require.NotEmpty(t, registration.RegistrationToken)

	sessionBody, err := json.Marshal(map[string]string{"sessionId": sessionID})
	require.NoError(t, err)
	sessionReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/runners/"+runnerID+"/sessions", bytes.NewBuffer(sessionBody))
	setAutomationRunnerHeaders(sessionReq, "Bearer "+registration.RegistrationToken, workspaceID)
	sessionRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(sessionRec, sessionReq)
	require.Equal(t, http.StatusCreated, sessionRec.Code, sessionRec.Body.String())
	var session automationRunnerSession
	require.NoError(t, json.Unmarshal(sessionRec.Body.Bytes(), &session))
	require.Equal(t, sessionID, session.SessionID)
	require.NotEmpty(t, session.SessionToken)
	return "Bearer " + session.SessionToken
}

func testSHA256Hex(payload []byte) string {
	sum := sha256.Sum256(payload)
	return hex.EncodeToString(sum[:])
}

func acquireAutomationRunnerJob(t *testing.T, srv *Server, auth string, workspaceID string, runnerID string, sessionID string) automationRunnerAcquiredJob {
	t.Helper()
	jobs := acquireAutomationRunnerJobsWithQuery(t, srv, auth, workspaceID, runnerID, sessionID, "")
	require.Len(t, jobs, 1)
	require.NotZero(t, jobs[0].QueueJobID)
	return jobs[0]
}

func acquireAutomationRunnerJobWithQuery(t *testing.T, srv *Server, auth string, workspaceID string, runnerID string, sessionID string, query string) automationRunnerAcquiredJob {
	t.Helper()
	jobs := acquireAutomationRunnerJobsWithQuery(t, srv, auth, workspaceID, runnerID, sessionID, query)
	require.Len(t, jobs, 1)
	require.NotZero(t, jobs[0].QueueJobID)
	return jobs[0]
}

func acquireAutomationRunnerJobsWithQuery(t *testing.T, srv *Server, auth string, workspaceID string, runnerID string, sessionID string, query string) []automationRunnerAcquiredJob {
	t.Helper()
	rawQuery := "sessionId=" + sessionID + "&limit=1"
	if !strings.Contains(query, "leaseSeconds=") {
		rawQuery += "&leaseSeconds=300"
	}
	if query != "" {
		rawQuery += "&" + query
	}
	acquireReq := httptest.NewRequest(http.MethodGet, "/api/v1/automations/runners/"+runnerID+"/jobs:acquire?"+rawQuery, nil)
	setAutomationRunnerHeaders(acquireReq, auth, workspaceID)
	acquireRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(acquireRec, acquireReq)
	require.Equal(t, http.StatusOK, acquireRec.Code, acquireRec.Body.String())
	var response struct {
		Jobs []automationRunnerAcquiredJob `json:"jobs"`
	}
	require.NoError(t, json.Unmarshal(acquireRec.Body.Bytes(), &response))
	return response.Jobs
}

func completeAutomationRunnerJob(t *testing.T, srv *Server, auth string, workspaceID string, runnerID string, sessionID string, queueJobID int64, outputJSON string) {
	t.Helper()
	completeReq := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/automations/runners/%s/jobs/%d/completed", runnerID, queueJobID), bytes.NewBufferString(fmt.Sprintf(`{"sessionId":%q,"outputJson":%q}`, sessionID, outputJSON)))
	setAutomationRunnerHeaders(completeReq, auth, workspaceID)
	completeRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(completeRec, completeReq)
	require.Equal(t, http.StatusOK, completeRec.Code, completeRec.Body.String())
	require.Contains(t, completeRec.Body.String(), `"status":"succeeded"`)
}

func setAutomationRunnerHeaders(req *http.Request, auth string, workspaceID string) {
	req.Header.Set("Authorization", auth)
	req.Header.Set("X-Workspace-Id", workspaceID)
}
