// Package testharness provides a reusable router integration harness with one
// explicit tenant capability per fixture. It exercises the real transport
// constructors, middleware, and routes, but it does not replace server-level
// tests of the production tenant registry and shared service graph.
package testharness

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

	"github.com/go-chi/chi/v5"
	"gorm.io/gorm"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/githttp"
	"github.com/pingcap/agent-git-service/internal/graphql"
	"github.com/pingcap/agent-git-service/internal/oauth"
	"github.com/pingcap/agent-git-service/internal/rest"
	"github.com/pingcap/agent-git-service/internal/router"
	"github.com/pingcap/agent-git-service/internal/service"
	"github.com/pingcap/agent-git-service/internal/tenant"
)

// Harness holds the fully-wired test infrastructure.
type Harness struct {
	Svc     *service.Service // for direct service-layer calls in test setup
	DB      *gorm.DB         // for direct DB seeding/assertions
	Mux     http.Handler     // host-aware mux from router.RegisterRoutes
	User    db.User          // pre-seeded default user (SiteAdmin: true)
	Token   string           // pre-seeded auth token value
	BaseURL string           // e.g. "http://localhost:8080"
	Tenant  *TenantHandle    // explicit capability for direct service calls

	srv     *httptest.Server
	srvOnce sync.Once
}

// New creates a router test harness with an isolated TiDB database, temp
// gitstore directory, real service layer, and production route registration.
// All resources are cleaned up via tb.Cleanup.
func New(tb testing.TB) *Harness {
	return NewWithServiceConfig(tb, ServiceConfig{})
}

// NewWithServiceConfig creates a full HTTP harness while allowing immutable
// tenant resources to be configured before the registry is published.
func NewWithServiceConfig(tb testing.TB, cfg ServiceConfig) *Harness {
	tb.Helper()

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		tb.Fatalf("testharness: reserve server listener: %v", err)
	}
	baseURL := "http://" + listener.Addr().String()

	cfg.CanonicalBaseURL = baseURL
	svc, tenantHandle, cleanup := NewService(tb, cfg)
	tb.Cleanup(cleanup)

	// Seed a default admin user (SiteAdmin: true for OAuth exchange compatibility).
	user := db.User{Login: "testuser", Name: "testuser", Type: db.TypeUser, UserKind: db.UserKindHuman, SiteAdmin: true}
	if err := tenantHandle.DB().Create(&user).Error; err != nil {
		tb.Fatalf("testharness: seed user: %v", err)
	}

	tokenValue := "test-token"
	if err := tenantHandle.DB().Create(&db.Token{UserID: user.ID, Value: tokenValue}).Error; err != nil {
		tb.Fatalf("testharness: seed token: %v", err)
	}

	// Wire handlers using production constructors.
	handlers := &rest.Deps{Svc: svc}
	gqlSrv := graphql.NewServer(svc)
	gitHandler := githttp.New(tenantHandle.Git(), svc)
	oauthHandler := oauth.New(svc)

	mux := router.RegisterRoutes(chi.NewRouter(), handlers, gitHandler, gqlSrv, oauthHandler)

	h := &Harness{
		Svc:     svc,
		DB:      tenantHandle.DB(),
		User:    user,
		Token:   tokenValue,
		BaseURL: baseURL,
		Tenant:  tenantHandle,
	}
	h.Mux = h.wrapTenant(mux)
	h.srv = &httptest.Server{
		Listener: listener,
		Config:   &http.Server{Handler: h.Mux},
	}
	tb.Cleanup(h.srv.Close)

	return h
}

// wrapTenant binds the same validated tenant context expected by production.
func (h *Harness) wrapTenant(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := h.Tenant.Bind(r.Context())
		ctx = tenant.WithResolvedHost(ctx, strings.ToLower(strings.TrimSpace(r.Host)))
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// Server lazily starts the listener reserved by New. Tenant identity and URL
// policy are immutable: the listener address was part of the fixture before
// the service registry was published.
//
// Server is idempotent: the first call starts the reserved listener; subsequent
// calls return the same server without mutation.
//
// Cleanup is registered on the root benchmark/test — not the caller's tb — so
// sibling subtests sharing a harness cannot close the server prematurely.
func (h *Harness) Server(tb testing.TB) *httptest.Server {
	tb.Helper()
	h.srvOnce.Do(func() {
		h.srv.Start()
	})
	return h.srv
}

// DoREST sends an authenticated HTTP request through the mux and returns
// the response recorder. It sets Authorization and Content-Type headers
// automatically. Callers assert the status code themselves.
func (h *Harness) DoREST(tb testing.TB, method, path string, body io.Reader) *httptest.ResponseRecorder {
	tb.Helper()
	req := httptest.NewRequest(method, path, body)
	req.Header.Set("Authorization", "token "+h.Token)
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	h.Mux.ServeHTTP(w, req)
	return w
}

// DoRESTJSON sends an authenticated HTTP request with an auto-marshaled JSON body.
// If body is nil, no request body is sent.
func (h *Harness) DoRESTJSON(tb testing.TB, method, path string, body any) *httptest.ResponseRecorder {
	tb.Helper()
	var r io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			tb.Fatalf("testharness: marshal body: %v", err)
		}
		r = bytes.NewReader(b)
	}
	return h.DoREST(tb, method, path, r)
}

// DoRESTWithHost performs an authenticated HTTP request with a custom Host header.
func (h *Harness) DoRESTWithHost(tb testing.TB, method, host, path string, body any) *httptest.ResponseRecorder {
	tb.Helper()
	var r io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			tb.Fatalf("testharness: marshal body: %v", err)
		}
		r = bytes.NewReader(b)
	}
	req := httptest.NewRequest(method, path, r)
	req.Host = host
	req.Header.Set("Authorization", "token "+h.Token)
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	h.Mux.ServeHTTP(w, req)
	return w
}

// DoRESTNoAuth performs an HTTP request without an Authorization header.
func (h *Harness) DoRESTNoAuth(tb testing.TB, method, path string) *httptest.ResponseRecorder {
	tb.Helper()
	req := httptest.NewRequest(method, path, nil)
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	h.Mux.ServeHTTP(w, req)
	return w
}

// DoRESTNoAuthWithHost performs an unauthenticated HTTP request with a custom Host header.
func (h *Harness) DoRESTNoAuthWithHost(tb testing.TB, method, host, path string) *httptest.ResponseRecorder {
	tb.Helper()
	req := httptest.NewRequest(method, path, nil)
	req.Host = host
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	h.Mux.ServeHTTP(w, req)
	return w
}

// DoRESTWithToken performs an HTTP request with a custom Authorization token.
func (h *Harness) DoRESTWithToken(tb testing.TB, method, path, token string) *httptest.ResponseRecorder {
	tb.Helper()
	req := httptest.NewRequest(method, path, nil)
	req.Header.Set("Authorization", "token "+token)
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	h.Mux.ServeHTTP(w, req)
	return w
}

// DoRESTJSONWithToken performs an HTTP request with a custom Authorization token
// and an auto-marshaled JSON body.
func (h *Harness) DoRESTJSONWithToken(tb testing.TB, method, path, token string, body any) *httptest.ResponseRecorder {
	tb.Helper()
	var r io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			tb.Fatalf("testharness: marshal body: %v", err)
		}
		r = bytes.NewReader(b)
	}
	req := httptest.NewRequest(method, path, r)
	req.Header.Set("Authorization", "token "+token)
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	h.Mux.ServeHTTP(w, req)
	return w
}

// DecodeJSON decodes a recorder's response body into a map.
func DecodeJSON(tb testing.TB, w *httptest.ResponseRecorder) map[string]any {
	tb.Helper()
	var result map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
		tb.Fatalf("decode json: %v\nbody: %s", err, w.Body.String())
	}
	return result
}

// DecodeJSONArray decodes a recorder's response body into a slice of maps.
func DecodeJSONArray(tb testing.TB, w *httptest.ResponseRecorder) []map[string]any {
	tb.Helper()
	var result []any
	if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
		tb.Fatalf("decode json array: %v\nbody: %s", err, w.Body.String())
	}
	out := make([]map[string]any, len(result))
	for i, v := range result {
		m, ok := v.(map[string]any)
		if !ok {
			tb.Fatalf("decode json array: element %d is not an object", i)
		}
		out[i] = m
	}
	return out
}

// DoGraphQL sends a GraphQL request through the mux, asserts 200 status,
// fails on GraphQL-level errors, and returns the "data" map.
func (h *Harness) DoGraphQL(tb testing.TB, query string, vars map[string]any) map[string]any {
	tb.Helper()
	reqBody := map[string]any{"query": query}
	if vars != nil {
		reqBody["variables"] = vars
	}
	b, err := json.Marshal(reqBody)
	if err != nil {
		tb.Fatalf("testharness: marshal graphql request: %v", err)
	}

	w := h.DoREST(tb, "POST", "/graphql", bytes.NewReader(b))
	if w.Code != http.StatusOK {
		tb.Fatalf("testharness: GraphQL returned %d: %s", w.Code, w.Body.String())
	}

	var res map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil {
		tb.Fatalf("testharness: unmarshal graphql response: %v", err)
	}
	if errs, ok := res["errors"]; ok {
		eb, _ := json.MarshalIndent(errs, "", "  ")
		tb.Fatalf("testharness: GraphQL errors: %s", string(eb))
	}
	data, ok := res["data"].(map[string]any)
	if !ok {
		tb.Fatalf("testharness: GraphQL response missing 'data' key: %s", w.Body.String())
	}
	return data
}
