package main

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

	"git.mchus.pro/mchus/quoteforge/internal/config"
	"git.mchus.pro/mchus/quoteforge/internal/db"
	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/models"
	"git.mchus.pro/mchus/quoteforge/internal/services"
	syncsvc "git.mchus.pro/mchus/quoteforge/internal/services/sync"
)

func TestConfigurationVersioningAPI(t *testing.T) {
	moveToRepoRoot(t)

	local, connMgr, configService := newAPITestStack(t)
	_ = local

	created, err := configService.Create("tester", &services.CreateConfigRequest{
		Name:        "api-v1",
		Items:       models.ConfigItems{{LotName: "CPU_API", Quantity: 1, UnitPrice: 1000}},
		ServerCount: 1,
	})
	if err != nil {
		t.Fatalf("create config: %v", err)
	}
	if _, err := configService.RenameNoAuth(created.UUID, "api-v2"); err != nil {
		t.Fatalf("rename config: %v", err)
	}

	cfg := &config.Config{}
	setConfigDefaults(cfg)
	router, _, err := setupRouter(cfg, local, connMgr, "tester", nil)
	if err != nil {
		t.Fatalf("setup router: %v", err)
	}

	// list versions happy path
	listReq := httptest.NewRequest(http.MethodGet, "/api/configs/"+created.UUID+"/versions?limit=10&offset=0", nil)
	listRec := httptest.NewRecorder()
	router.ServeHTTP(listRec, listReq)
	if listRec.Code != http.StatusOK {
		t.Fatalf("list versions status=%d body=%s", listRec.Code, listRec.Body.String())
	}

	// get version happy path
	getReq := httptest.NewRequest(http.MethodGet, "/api/configs/"+created.UUID+"/versions/1", nil)
	getRec := httptest.NewRecorder()
	router.ServeHTTP(getRec, getReq)
	if getRec.Code != http.StatusOK {
		t.Fatalf("get version status=%d body=%s", getRec.Code, getRec.Body.String())
	}

	// rollback happy path
	body := []byte(`{"target_version":1,"note":"api rollback"}`)
	rbReq := httptest.NewRequest(http.MethodPost, "/api/configs/"+created.UUID+"/rollback", bytes.NewReader(body))
	rbReq.Header.Set("Content-Type", "application/json")
	rbRec := httptest.NewRecorder()
	router.ServeHTTP(rbRec, rbReq)
	if rbRec.Code != http.StatusOK {
		t.Fatalf("rollback status=%d body=%s", rbRec.Code, rbRec.Body.String())
	}

	var rbResp struct {
		Message        string `json:"message"`
		CurrentVersion struct {
			VersionNo int `json:"version_no"`
		} `json:"current_version"`
	}
	if err := json.Unmarshal(rbRec.Body.Bytes(), &rbResp); err != nil {
		t.Fatalf("unmarshal rollback response: %v", err)
	}
	if rbResp.Message == "" || rbResp.CurrentVersion.VersionNo != 2 {
		t.Fatalf("unexpected rollback response: %+v", rbResp)
	}

	// 404: version missing
	notFoundReq := httptest.NewRequest(http.MethodGet, "/api/configs/"+created.UUID+"/versions/999", nil)
	notFoundRec := httptest.NewRecorder()
	router.ServeHTTP(notFoundRec, notFoundReq)
	if notFoundRec.Code != http.StatusNotFound {
		t.Fatalf("expected 404 for missing version, got %d", notFoundRec.Code)
	}

	// 400: invalid version number
	invalidReq := httptest.NewRequest(http.MethodGet, "/api/configs/"+created.UUID+"/versions/abc", nil)
	invalidRec := httptest.NewRecorder()
	router.ServeHTTP(invalidRec, invalidReq)
	if invalidRec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for invalid version, got %d", invalidRec.Code)
	}

	// 400: rollback invalid target_version
	badRollbackReq := httptest.NewRequest(http.MethodPost, "/api/configs/"+created.UUID+"/rollback", bytes.NewReader([]byte(`{"target_version":0}`)))
	badRollbackReq.Header.Set("Content-Type", "application/json")
	badRollbackRec := httptest.NewRecorder()
	router.ServeHTTP(badRollbackRec, badRollbackReq)
	if badRollbackRec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for invalid rollback target, got %d", badRollbackRec.Code)
	}

	// archive + reactivate flow
	delReq := httptest.NewRequest(http.MethodDelete, "/api/configs/"+created.UUID, nil)
	delRec := httptest.NewRecorder()
	router.ServeHTTP(delRec, delReq)
	if delRec.Code != http.StatusOK {
		t.Fatalf("archive status=%d body=%s", delRec.Code, delRec.Body.String())
	}

	archivedListReq := httptest.NewRequest(http.MethodGet, "/api/configs?status=archived&page=1&per_page=20", nil)
	archivedListRec := httptest.NewRecorder()
	router.ServeHTTP(archivedListRec, archivedListReq)
	if archivedListRec.Code != http.StatusOK {
		t.Fatalf("archived list status=%d body=%s", archivedListRec.Code, archivedListRec.Body.String())
	}

	reactivateReq := httptest.NewRequest(http.MethodPost, "/api/configs/"+created.UUID+"/reactivate", nil)
	reactivateRec := httptest.NewRecorder()
	router.ServeHTTP(reactivateRec, reactivateReq)
	if reactivateRec.Code != http.StatusOK {
		t.Fatalf("reactivate status=%d body=%s", reactivateRec.Code, reactivateRec.Body.String())
	}

	activeListReq := httptest.NewRequest(http.MethodGet, "/api/configs?status=active&page=1&per_page=20", nil)
	activeListRec := httptest.NewRecorder()
	router.ServeHTTP(activeListRec, activeListReq)
	if activeListRec.Code != http.StatusOK {
		t.Fatalf("active list status=%d body=%s", activeListRec.Code, activeListRec.Body.String())
	}
}

func TestProjectArchiveHidesConfigsAndCloneIntoProject(t *testing.T) {
	moveToRepoRoot(t)

	local, connMgr, configService := newAPITestStack(t)
	_ = configService

	cfg := &config.Config{}
	setConfigDefaults(cfg)
	router, _, err := setupRouter(cfg, local, connMgr, "tester", nil)
	if err != nil {
		t.Fatalf("setup router: %v", err)
	}

	createProjectReq := httptest.NewRequest(http.MethodPost, "/api/projects", bytes.NewReader([]byte(`{"name":"P1","code":"P1"}`)))
	createProjectReq.Header.Set("Content-Type", "application/json")
	createProjectRec := httptest.NewRecorder()
	router.ServeHTTP(createProjectRec, createProjectReq)
	if createProjectRec.Code != http.StatusCreated {
		t.Fatalf("create project status=%d body=%s", createProjectRec.Code, createProjectRec.Body.String())
	}
	var project models.Project
	if err := json.Unmarshal(createProjectRec.Body.Bytes(), &project); err != nil {
		t.Fatalf("unmarshal project: %v", err)
	}

	createCfgBody := []byte(`{"name":"Cfg A","items":[{"lot_name":"CPU","quantity":1,"unit_price":100}],"server_count":1}`)
	createCfgReq := httptest.NewRequest(http.MethodPost, "/api/projects/"+project.UUID+"/configs", bytes.NewReader(createCfgBody))
	createCfgReq.Header.Set("Content-Type", "application/json")
	createCfgRec := httptest.NewRecorder()
	router.ServeHTTP(createCfgRec, createCfgReq)
	if createCfgRec.Code != http.StatusCreated {
		t.Fatalf("create project config status=%d body=%s", createCfgRec.Code, createCfgRec.Body.String())
	}
	var createdCfg models.Configuration
	if err := json.Unmarshal(createCfgRec.Body.Bytes(), &createdCfg); err != nil {
		t.Fatalf("unmarshal project config: %v", err)
	}
	if createdCfg.ProjectUUID == nil || *createdCfg.ProjectUUID != project.UUID {
		t.Fatalf("expected config project_uuid=%s got=%v", project.UUID, createdCfg.ProjectUUID)
	}

	cloneReq := httptest.NewRequest(http.MethodPost, "/api/projects/"+project.UUID+"/configs/"+createdCfg.UUID+"/clone", bytes.NewReader([]byte(`{"name":"Cfg A Clone"}`)))
	cloneReq.Header.Set("Content-Type", "application/json")
	cloneRec := httptest.NewRecorder()
	router.ServeHTTP(cloneRec, cloneReq)
	if cloneRec.Code != http.StatusCreated {
		t.Fatalf("clone in project status=%d body=%s", cloneRec.Code, cloneRec.Body.String())
	}
	var cloneCfg models.Configuration
	if err := json.Unmarshal(cloneRec.Body.Bytes(), &cloneCfg); err != nil {
		t.Fatalf("unmarshal clone config: %v", err)
	}
	if cloneCfg.ProjectUUID == nil || *cloneCfg.ProjectUUID != project.UUID {
		t.Fatalf("expected clone project_uuid=%s got=%v", project.UUID, cloneCfg.ProjectUUID)
	}

	projectConfigsReq := httptest.NewRequest(http.MethodGet, "/api/projects/"+project.UUID+"/configs", nil)
	projectConfigsRec := httptest.NewRecorder()
	router.ServeHTTP(projectConfigsRec, projectConfigsReq)
	if projectConfigsRec.Code != http.StatusOK {
		t.Fatalf("project configs status=%d body=%s", projectConfigsRec.Code, projectConfigsRec.Body.String())
	}
	var projectConfigsResp struct {
		Configurations []models.Configuration `json:"configurations"`
	}
	if err := json.Unmarshal(projectConfigsRec.Body.Bytes(), &projectConfigsResp); err != nil {
		t.Fatalf("unmarshal project configs response: %v", err)
	}
	if len(projectConfigsResp.Configurations) != 2 {
		t.Fatalf("expected 2 project configs after clone, got %d", len(projectConfigsResp.Configurations))
	}

	archiveReq := httptest.NewRequest(http.MethodPost, "/api/projects/"+project.UUID+"/archive", nil)
	archiveRec := httptest.NewRecorder()
	router.ServeHTTP(archiveRec, archiveReq)
	if archiveRec.Code != http.StatusOK {
		t.Fatalf("archive project status=%d body=%s", archiveRec.Code, archiveRec.Body.String())
	}

	activeReq := httptest.NewRequest(http.MethodGet, "/api/configs?status=active&page=1&per_page=20", nil)
	activeRec := httptest.NewRecorder()
	router.ServeHTTP(activeRec, activeReq)
	if activeRec.Code != http.StatusOK {
		t.Fatalf("active configs status=%d body=%s", activeRec.Code, activeRec.Body.String())
	}
	var activeResp struct {
		Configurations []models.Configuration `json:"configurations"`
	}
	if err := json.Unmarshal(activeRec.Body.Bytes(), &activeResp); err != nil {
		t.Fatalf("unmarshal active configs response: %v", err)
	}
	if len(activeResp.Configurations) != 0 {
		t.Fatalf("expected no active configs after project archive, got %d", len(activeResp.Configurations))
	}
}

func TestConfigMoveToProjectEndpoint(t *testing.T) {
	moveToRepoRoot(t)

	local, connMgr, _ := newAPITestStack(t)
	cfg := &config.Config{}
	setConfigDefaults(cfg)
	router, _, err := setupRouter(cfg, local, connMgr, "tester", nil)
	if err != nil {
		t.Fatalf("setup router: %v", err)
	}

	createProjectReq := httptest.NewRequest(http.MethodPost, "/api/projects", bytes.NewReader([]byte(`{"name":"Move Project","code":"MOVE"}`)))
	createProjectReq.Header.Set("Content-Type", "application/json")
	createProjectRec := httptest.NewRecorder()
	router.ServeHTTP(createProjectRec, createProjectReq)
	if createProjectRec.Code != http.StatusCreated {
		t.Fatalf("create project status=%d body=%s", createProjectRec.Code, createProjectRec.Body.String())
	}
	var project models.Project
	if err := json.Unmarshal(createProjectRec.Body.Bytes(), &project); err != nil {
		t.Fatalf("unmarshal project: %v", err)
	}

	createConfigReq := httptest.NewRequest(http.MethodPost, "/api/configs", bytes.NewReader([]byte(`{"name":"Move Me","items":[],"notes":"","server_count":1}`)))
	createConfigReq.Header.Set("Content-Type", "application/json")
	createConfigRec := httptest.NewRecorder()
	router.ServeHTTP(createConfigRec, createConfigReq)
	if createConfigRec.Code != http.StatusCreated {
		t.Fatalf("create config status=%d body=%s", createConfigRec.Code, createConfigRec.Body.String())
	}
	var created models.Configuration
	if err := json.Unmarshal(createConfigRec.Body.Bytes(), &created); err != nil {
		t.Fatalf("unmarshal config: %v", err)
	}

	moveReq := httptest.NewRequest(http.MethodPatch, "/api/configs/"+created.UUID+"/project", bytes.NewReader([]byte(`{"project_uuid":"`+project.UUID+`"}`)))
	moveReq.Header.Set("Content-Type", "application/json")
	moveRec := httptest.NewRecorder()
	router.ServeHTTP(moveRec, moveReq)
	if moveRec.Code != http.StatusOK {
		t.Fatalf("move config status=%d body=%s", moveRec.Code, moveRec.Body.String())
	}

	getReq := httptest.NewRequest(http.MethodGet, "/api/configs/"+created.UUID, nil)
	getRec := httptest.NewRecorder()
	router.ServeHTTP(getRec, getReq)
	if getRec.Code != http.StatusOK {
		t.Fatalf("get config status=%d body=%s", getRec.Code, getRec.Body.String())
	}
	var updated models.Configuration
	if err := json.Unmarshal(getRec.Body.Bytes(), &updated); err != nil {
		t.Fatalf("unmarshal updated config: %v", err)
	}
	if updated.ProjectUUID == nil || *updated.ProjectUUID != project.UUID {
		t.Fatalf("expected moved project_uuid=%s, got %v", project.UUID, updated.ProjectUUID)
	}
}

func TestVendorImportRejectsOversizedUpload(t *testing.T) {
	moveToRepoRoot(t)

	prevLimit := vendorImportMaxBytes
	vendorImportMaxBytes = 128
	defer func() { vendorImportMaxBytes = prevLimit }()

	local, connMgr, _ := newAPITestStack(t)
	cfg := &config.Config{}
	setConfigDefaults(cfg)
	router, _, err := setupRouter(cfg, local, connMgr, "tester", nil)
	if err != nil {
		t.Fatalf("setup router: %v", err)
	}

	createProjectReq := httptest.NewRequest(http.MethodPost, "/api/projects", bytes.NewReader([]byte(`{"name":"Import Project","code":"IMP"}`)))
	createProjectReq.Header.Set("Content-Type", "application/json")
	createProjectRec := httptest.NewRecorder()
	router.ServeHTTP(createProjectRec, createProjectReq)
	if createProjectRec.Code != http.StatusCreated {
		t.Fatalf("create project status=%d body=%s", createProjectRec.Code, createProjectRec.Body.String())
	}

	var project models.Project
	if err := json.Unmarshal(createProjectRec.Body.Bytes(), &project); err != nil {
		t.Fatalf("unmarshal project: %v", err)
	}

	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	part, err := writer.CreateFormFile("file", "huge.xml")
	if err != nil {
		t.Fatalf("create form file: %v", err)
	}
	payload := "<CFXML>" + strings.Repeat("A", int(vendorImportMaxBytes)+1) + "</CFXML>"
	if _, err := part.Write([]byte(payload)); err != nil {
		t.Fatalf("write multipart payload: %v", err)
	}
	if err := writer.Close(); err != nil {
		t.Fatalf("close multipart writer: %v", err)
	}

	req := httptest.NewRequest(http.MethodPost, "/api/projects/"+project.UUID+"/vendor-import", &body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for oversized upload, got %d body=%s", rec.Code, rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "1 GiB") {
		t.Fatalf("expected size limit message, got %s", rec.Body.String())
	}
}

func TestCreateConfigMalformedJSONReturnsGenericError(t *testing.T) {
	moveToRepoRoot(t)

	local, connMgr, _ := newAPITestStack(t)
	cfg := &config.Config{}
	setConfigDefaults(cfg)
	router, _, err := setupRouter(cfg, local, connMgr, "tester", nil)
	if err != nil {
		t.Fatalf("setup router: %v", err)
	}

	req := httptest.NewRequest(http.MethodPost, "/api/configs", bytes.NewReader([]byte(`{"name":`)))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for malformed json, got %d body=%s", rec.Code, rec.Body.String())
	}
	if strings.Contains(strings.ToLower(rec.Body.String()), "unexpected eof") {
		t.Fatalf("expected sanitized error body, got %s", rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "invalid request") {
		t.Fatalf("expected generic invalid request message, got %s", rec.Body.String())
	}
}

func newAPITestStack(t *testing.T) (*localdb.LocalDB, *db.ConnectionManager, *services.LocalConfigurationService) {
	t.Helper()

	localPath := filepath.Join(t.TempDir(), "api.db")
	local, err := localdb.New(localPath)
	if err != nil {
		t.Fatalf("init local db: %v", err)
	}
	t.Cleanup(func() { _ = local.Close() })

	connMgr := db.NewConnectionManager(local)
	syncService := syncsvc.NewService(connMgr, local)
	configService := services.NewLocalConfigurationService(
		local,
		syncService,
		&services.QuoteService{},
		func() bool { return false },
	)
	return local, connMgr, configService
}

func moveToRepoRoot(t *testing.T) {
	t.Helper()
	wd, err := os.Getwd()
	if err != nil {
		t.Fatalf("getwd: %v", err)
	}
	root := filepath.Clean(filepath.Join(wd, "..", ".."))
	if err := os.Chdir(root); err != nil {
		t.Fatalf("chdir repo root: %v", err)
	}
	t.Cleanup(func() {
		_ = os.Chdir(wd)
	})
}
