package server

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"git.mchus.pro/mchus/logpile/internal/models"
)

func newCollectTestServer() (*Server, *httptest.Server) {
	s := &Server{
		jobManager: NewJobManager(),
		collectors: testCollectorRegistry(),
	}
	mux := http.NewServeMux()
	mux.HandleFunc("POST /api/collect/probe", s.handleCollectProbe)
	mux.HandleFunc("POST /api/collect", s.handleCollectStart)
	mux.HandleFunc("GET /api/collect/{id}", s.handleCollectStatus)
	mux.HandleFunc("POST /api/collect/{id}/cancel", s.handleCollectCancel)
	mux.HandleFunc("POST /api/collect/{id}/skip", s.handleCollectSkip)
	return s, httptest.NewServer(mux)
}

func TestCollectProbe(t *testing.T) {
	_, ts := newCollectTestServer()
	defer ts.Close()

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen probe target: %v", err)
	}
	defer ln.Close()
	addr, ok := ln.Addr().(*net.TCPAddr)
	if !ok {
		t.Fatalf("unexpected listener address type: %T", ln.Addr())
	}

	body := fmt.Sprintf(`{"host":"127.0.0.1","protocol":"redfish","port":%d,"username":"admin-off","auth_type":"password","password":"secret","tls_mode":"strict"}`, addr.Port)
	resp, err := http.Post(ts.URL+"/api/collect/probe", "application/json", bytes.NewBufferString(body))
	if err != nil {
		t.Fatalf("post collect probe failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}

	var payload CollectProbeResponse
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		t.Fatalf("decode probe response: %v", err)
	}
	if !payload.Reachable {
		t.Fatalf("expected reachable=true, got false")
	}
	if payload.HostPoweredOn {
		t.Fatalf("expected host powered off in probe response")
	}
	if payload.HostPowerState != "Off" {
		t.Fatalf("expected host power state Off, got %q", payload.HostPowerState)
	}
}

func TestCollectLifecycleToTerminal(t *testing.T) {
	_, ts := newCollectTestServer()
	defer ts.Close()

	body := `{"host":"bmc01.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
	resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
	if err != nil {
		t.Fatalf("post collect failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusAccepted {
		t.Fatalf("expected 202, got %d", resp.StatusCode)
	}

	var created CollectJobResponse
	if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
		t.Fatalf("decode create response: %v", err)
	}
	if created.JobID == "" {
		t.Fatalf("expected job id")
	}

	status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
	if status.Status != CollectStatusSuccess {
		t.Fatalf("expected success, got %q (error=%q)", status.Status, status.Error)
	}
	if status.Progress == nil || *status.Progress != 100 {
		t.Fatalf("expected progress 100, got %#v", status.Progress)
	}
	if len(status.Logs) < 4 {
		t.Fatalf("expected detailed logs, got %v", status.Logs)
	}
	if len(status.ActiveModules) == 0 {
		t.Fatal("expected active modules in collect status")
	}
	if status.ActiveModules[0].Name == "" {
		t.Fatal("expected active module name")
	}
	if len(status.ModuleScores) == 0 {
		t.Fatal("expected module scores in collect status")
	}
	if status.DebugInfo == nil {
		t.Fatal("expected debug info in collect status")
	}
	if len(status.DebugInfo.PhaseTelemetry) == 0 {
		t.Fatal("expected phase telemetry in collect debug info")
	}
}

func TestCollectCancel(t *testing.T) {
	_, ts := newCollectTestServer()
	defer ts.Close()

	body := `{"host":"bmc02.local","protocol":"ipmi","port":623,"username":"operator","auth_type":"token","token":"keep-me-secret","tls_mode":"insecure"}`
	resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
	if err != nil {
		t.Fatalf("post collect failed: %v", err)
	}
	defer resp.Body.Close()

	var created CollectJobResponse
	if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
		t.Fatalf("decode create response: %v", err)
	}

	cancelResp, err := http.Post(ts.URL+"/api/collect/"+created.JobID+"/cancel", "application/json", nil)
	if err != nil {
		t.Fatalf("cancel collect failed: %v", err)
	}
	defer cancelResp.Body.Close()

	if cancelResp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200 cancel, got %d", cancelResp.StatusCode)
	}

	var canceled CollectJobStatusResponse
	if err := json.NewDecoder(cancelResp.Body).Decode(&canceled); err != nil {
		t.Fatalf("decode cancel response: %v", err)
	}
	if canceled.Status != CollectStatusCanceled {
		t.Fatalf("expected canceled, got %q", canceled.Status)
	}

	time.Sleep(500 * time.Millisecond)
	final := getCollectStatus(t, ts.URL, created.JobID, http.StatusOK)
	if final.Status != CollectStatusCanceled {
		t.Fatalf("expected canceled to stay terminal, got %q", final.Status)
	}
}

func TestCollectNotFoundAndSecretLeak(t *testing.T) {
	_, ts := newCollectTestServer()
	defer ts.Close()

	notFound := getCollectStatus(t, ts.URL, "job_notfound123", http.StatusNotFound)
	if notFound.JobID != "" || notFound.Status != "" {
		t.Fatalf("unexpected body for not found: %+v", notFound)
	}
	cancelResp, err := http.Post(ts.URL+"/api/collect/job_notfound123/cancel", "application/json", nil)
	if err != nil {
		t.Fatalf("cancel not found request failed: %v", err)
	}
	cancelResp.Body.Close()
	if cancelResp.StatusCode != http.StatusNotFound {
		t.Fatalf("expected 404 for cancel not found, got %d", cancelResp.StatusCode)
	}

	body := `{"host":"need-fail.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"ultra-secret","tls_mode":"strict"}`
	resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
	if err != nil {
		t.Fatalf("post collect failed: %v", err)
	}
	defer resp.Body.Close()

	var created CollectJobResponse
	if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
		t.Fatalf("decode create response: %v", err)
	}

	status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
	if status.Status != CollectStatusFailed {
		t.Fatalf("expected failed by host toggle, got %q", status.Status)
	}

	raw, err := json.Marshal(status)
	if err != nil {
		t.Fatalf("marshal status: %v", err)
	}
	if strings.Contains(string(raw), "ultra-secret") || strings.Contains(strings.Join(status.Logs, " "), "ultra-secret") {
		t.Fatalf("secret leaked into API response or logs")
	}
}

func TestCollectStartPreservesCurrentResultUntilSuccess(t *testing.T) {
	s, ts := newCollectTestServer()
	defer ts.Close()

	existing := &models.AnalysisResult{
		Filename:    "archive.tar.gz",
		SourceType:  models.SourceTypeArchive,
		CollectedAt: time.Now().UTC(),
	}
	s.SetResult(existing)

	body := `{"host":"bmc-success.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
	resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
	if err != nil {
		t.Fatalf("post collect failed: %v", err)
	}
	defer resp.Body.Close()

	var created CollectJobResponse
	if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
		t.Fatalf("decode create response: %v", err)
	}

	current := s.GetResult()
	if current != existing {
		t.Fatalf("expected current result to stay unchanged before success")
	}

	status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
	if status.Status != CollectStatusSuccess {
		t.Fatalf("expected success, got %q", status.Status)
	}

	finalResult := s.GetResult()
	if finalResult == nil {
		t.Fatalf("expected result to be set on success")
	}
	if finalResult.SourceType != models.SourceTypeAPI {
		t.Fatalf("expected api source type after success, got %q", finalResult.SourceType)
	}
	if finalResult.TargetHost != "bmc-success.local" {
		t.Fatalf("expected target host to be updated, got %q", finalResult.TargetHost)
	}
}

func TestCollectFailedDoesNotOverwriteCurrentResult(t *testing.T) {
	s, ts := newCollectTestServer()
	defer ts.Close()

	existing := &models.AnalysisResult{
		Filename:    "still-archive.tar.gz",
		SourceType:  models.SourceTypeArchive,
		CollectedAt: time.Now().UTC(),
	}
	s.SetResult(existing)

	body := `{"host":"contains-fail.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
	resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
	if err != nil {
		t.Fatalf("post collect failed: %v", err)
	}
	defer resp.Body.Close()

	var created CollectJobResponse
	if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
		t.Fatalf("decode create response: %v", err)
	}

	status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
	if status.Status != CollectStatusFailed {
		t.Fatalf("expected failed, got %q", status.Status)
	}

	finalResult := s.GetResult()
	if finalResult != existing {
		t.Fatalf("expected existing result to remain on failed job")
	}
}

func waitForTerminalStatus(t *testing.T, baseURL, jobID string, timeout time.Duration) CollectJobStatusResponse {
	t.Helper()
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		status := getCollectStatus(t, baseURL, jobID, http.StatusOK)
		if status.Status == CollectStatusSuccess || status.Status == CollectStatusFailed || status.Status == CollectStatusCanceled {
			return status
		}
		time.Sleep(100 * time.Millisecond)
	}
	t.Fatalf("job %s did not reach terminal status before timeout", jobID)
	return CollectJobStatusResponse{}
}

func getCollectStatus(t *testing.T, baseURL, jobID string, expectedCode int) CollectJobStatusResponse {
	t.Helper()
	resp, err := http.Get(baseURL + "/api/collect/" + jobID)
	if err != nil {
		t.Fatalf("get collect status failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != expectedCode {
		t.Fatalf("expected status %d, got %d", expectedCode, resp.StatusCode)
	}

	if expectedCode != http.StatusOK {
		return CollectJobStatusResponse{}
	}

	var status CollectJobStatusResponse
	if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
		t.Fatalf("decode collect status: %v", err)
	}
	return status
}
