package server

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

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

func TestApplyArchiveSourceMetadata(t *testing.T) {
	result := &models.AnalysisResult{}

	applyArchiveSourceMetadata(result)

	if result.SourceType != models.SourceTypeArchive {
		t.Fatalf("expected source type %q, got %q", models.SourceTypeArchive, result.SourceType)
	}
	if result.Protocol != "" {
		t.Fatalf("expected empty protocol for archive, got %q", result.Protocol)
	}
	if result.TargetHost != "" {
		t.Fatalf("expected empty target host for archive, got %q", result.TargetHost)
	}
	if result.CollectedAt.IsZero() {
		t.Fatalf("expected collected_at to be set")
	}
}

func TestApplyArchiveSourceMetadata_PreservesExistingCollectedAt(t *testing.T) {
	expected := time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC)
	result := &models.AnalysisResult{
		CollectedAt: expected,
	}

	applyArchiveSourceMetadata(result)

	if !result.CollectedAt.Equal(expected) {
		t.Fatalf("expected collected_at to be preserved: got %s want %s", result.CollectedAt, expected)
	}
}

func TestApplyArchiveSourceMetadata_InferCollectedAtFromEvents(t *testing.T) {
	oldTs := time.Date(2026, 2, 10, 13, 0, 0, 0, time.UTC)
	newTs := time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC)
	result := &models.AnalysisResult{
		Events: []models.Event{
			{Timestamp: oldTs},
			{Timestamp: newTs},
		},
	}

	applyArchiveSourceMetadata(result)

	if !result.CollectedAt.Equal(newTs) {
		t.Fatalf("expected collected_at from latest event: got %s want %s", result.CollectedAt, newTs)
	}
}

func TestApplyArchiveSourceMetadata_InferCollectedAtFromFilename(t *testing.T) {
	result := &models.AnalysisResult{
		Filename: "dump_23E100203_20260228-0428.tar.gz",
	}

	applyArchiveSourceMetadata(result)

	// 2026-02-28 04:28 in Europe/Moscow => 2026-02-28 01:28 UTC
	want := time.Date(2026, 2, 28, 1, 28, 0, 0, time.UTC)
	if !result.CollectedAt.Equal(want) {
		t.Fatalf("expected collected_at from filename: got %s want %s", result.CollectedAt, want)
	}
}

func TestApplyArchiveSourceMetadata_IgnoresSyntheticComponentNowEvents(t *testing.T) {
	realTs := time.Date(2026, 2, 28, 4, 18, 18, 217225000, time.FixedZone("UTC+8", 8*3600))
	syntheticNow := time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)
	result := &models.AnalysisResult{
		Events: []models.Event{
			{
				Timestamp: realTs,
				Source:    "spx_restservice_ext",
				SensorType:"syslog",
				EventType: "System Log",
			},
			{
				Timestamp:  syntheticNow,
				Source:     "Fan",
				SensorType: "fan",
				EventType:  "Fan Status",
			},
		},
	}

	applyArchiveSourceMetadata(result)

	if !result.CollectedAt.Equal(realTs.UTC()) {
		t.Fatalf("expected collected_at from real log timestamp: got %s want %s", result.CollectedAt, realTs.UTC())
	}
}

func TestInferRawExportCollectedAt_PrefersResultCollectedAt(t *testing.T) {
	expected := time.Date(2026, 2, 25, 8, 0, 0, 0, time.UTC)
	result := &models.AnalysisResult{CollectedAt: expected}
	pkg := &RawExportPackage{
		ExportedAt: time.Date(2026, 2, 25, 9, 59, 41, 0, time.UTC),
		Source: RawExportSource{
			CollectLogs: []string{
				"2026-02-25T09:00:00Z step1",
				"2026-02-25T09:10:00Z step2",
			},
		},
	}

	got := inferRawExportCollectedAt(result, pkg)
	if !got.Equal(expected) {
		t.Fatalf("expected collected_at from result: got %s want %s", got, expected)
	}
}

func TestInferRawExportCollectedAt_UsesCollectLogsThenExportedAt(t *testing.T) {
	hintTs := time.Date(2026, 2, 25, 9, 58, 5, 912975300, time.UTC)
	pkgWithLogs := &RawExportPackage{
		ExportedAt:      time.Date(2026, 2, 25, 9, 59, 41, 0, time.UTC),
		CollectedAtHint: hintTs,
		Source: RawExportSource{
			CollectLogs: []string{
				"2026-02-25T09:10:13.7442032Z started",
				"2026-02-25T09:31:00.5077486Z finished",
			},
		},
	}
	got := inferRawExportCollectedAt(&models.AnalysisResult{}, pkgWithLogs)
	if !got.Equal(hintTs) {
		t.Fatalf("expected collected_at from parser_fields hint: got %s want %s", got, hintTs)
	}

	pkgFromLogs := &RawExportPackage{
		ExportedAt: time.Date(2026, 2, 25, 9, 59, 41, 0, time.UTC),
		Source: RawExportSource{
			CollectLogs: []string{
				"2026-02-25T09:10:13.7442032Z started",
				"2026-02-25T09:31:00.5077486Z finished",
			},
		},
	}
	got = inferRawExportCollectedAt(&models.AnalysisResult{}, pkgFromLogs)
	wantFromLogs := time.Date(2026, 2, 25, 9, 31, 0, 507748600, time.UTC)
	if !got.Equal(wantFromLogs) {
		t.Fatalf("expected collected_at from collect logs: got %s want %s", got, wantFromLogs)
	}

	pkgWithoutLogs := &RawExportPackage{
		ExportedAt: time.Date(2026, 2, 25, 9, 59, 41, 479023400, time.UTC),
	}
	got = inferRawExportCollectedAt(&models.AnalysisResult{}, pkgWithoutLogs)
	wantFromExportedAt := time.Date(2026, 2, 25, 9, 59, 41, 479023400, time.UTC)
	if !got.Equal(wantFromExportedAt) {
		t.Fatalf("expected collected_at from exported_at: got %s want %s", got, wantFromExportedAt)
	}
}

func TestApplyCollectSourceMetadata(t *testing.T) {
	req := CollectRequest{
		Host:     "bmc-api.local",
		Protocol: "redfish",
		Port:     443,
		Username: "admin",
		AuthType: "password",
		Password: "super-secret",
		TLSMode:  "strict",
	}

	result := &models.AnalysisResult{
		Events:  make([]models.Event, 0),
		FRU:     make([]models.FRUInfo, 0),
		Sensors: make([]models.SensorReading, 0),
	}
	applyCollectSourceMetadata(result, req)

	if result.SourceType != models.SourceTypeAPI {
		t.Fatalf("expected source type %q, got %q", models.SourceTypeAPI, result.SourceType)
	}
	if result.Protocol != req.Protocol {
		t.Fatalf("expected protocol %q, got %q", req.Protocol, result.Protocol)
	}
	if result.TargetHost != req.Host {
		t.Fatalf("expected target host %q, got %q", req.Host, result.TargetHost)
	}
	if result.CollectedAt.IsZero() {
		t.Fatalf("expected collected_at to be set")
	}
	if len(result.Events) != 0 || len(result.FRU) != 0 || len(result.Sensors) != 0 {
		t.Fatalf("expected empty slices for api result")
	}

	raw, err := json.Marshal(result)
	if err != nil {
		t.Fatalf("marshal result: %v", err)
	}
	if string(raw) == "" {
		t.Fatalf("expected non-empty json")
	}
	if strings.Contains(string(raw), req.Password) || (req.Token != "" && strings.Contains(string(raw), req.Token)) {
		t.Fatalf("secrets should not be present in api result")
	}
}

func TestApplyCollectSourceMetadata_PreservesCollectedAtAndTimezone(t *testing.T) {
	req := CollectRequest{
		Host:     "bmc-api.local",
		Protocol: "redfish",
		Port:     443,
		Username: "admin",
		AuthType: "password",
		Password: "super-secret",
		TLSMode:  "strict",
	}
	collectedAt := time.Date(2026, 2, 28, 4, 18, 18, 0, time.FixedZone("UTC+8", 8*3600))
	result := &models.AnalysisResult{
		CollectedAt:    collectedAt,
		SourceTimezone: "+08:00",
	}

	applyCollectSourceMetadata(result, req)

	if !result.CollectedAt.Equal(collectedAt) {
		t.Fatalf("expected collected_at to be preserved: got %s want %s", result.CollectedAt, collectedAt)
	}
	if result.SourceTimezone != "+08:00" {
		t.Fatalf("expected source_timezone to be preserved, got %q", result.SourceTimezone)
	}
}

func TestStatusAndConfigExposeSourceMetadata(t *testing.T) {
	s := &Server{}
	s.SetDetectedVendor("nvidia")
	s.SetResult(&models.AnalysisResult{
		Filename:    "archive.tar.gz",
		SourceType:  models.SourceTypeArchive,
		Protocol:    "",
		TargetHost:  "",
		CollectedAt: time.Now().UTC(),
		Events:      []models.Event{{ID: "1"}},
		Sensors:     []models.SensorReading{{Name: "Temp1"}},
		FRU:         []models.FRUInfo{{Description: "Board"}},
	})

	statusReq := httptest.NewRequest(http.MethodGet, "/api/status", nil)
	statusRec := httptest.NewRecorder()
	s.handleGetStatus(statusRec, statusReq)
	if statusRec.Code != http.StatusOK {
		t.Fatalf("expected 200 from /api/status, got %d", statusRec.Code)
	}

	var statusPayload map[string]interface{}
	if err := json.NewDecoder(statusRec.Body).Decode(&statusPayload); err != nil {
		t.Fatalf("decode status payload: %v", err)
	}

	if loaded, _ := statusPayload["loaded"].(bool); !loaded {
		t.Fatalf("expected loaded=true")
	}
	if statusPayload["source_type"] != models.SourceTypeArchive {
		t.Fatalf("expected source_type in status payload")
	}
	if _, ok := statusPayload["stats"]; !ok {
		t.Fatalf("expected legacy stats field to remain")
	}

	configReq := httptest.NewRequest(http.MethodGet, "/api/config", nil)
	configRec := httptest.NewRecorder()
	s.handleGetConfig(configRec, configReq)
	if configRec.Code != http.StatusOK {
		t.Fatalf("expected 200 from /api/config, got %d", configRec.Code)
	}

	var configPayload map[string]interface{}
	if err := json.NewDecoder(configRec.Body).Decode(&configPayload); err != nil {
		t.Fatalf("decode config payload: %v", err)
	}

	if configPayload["source_type"] != models.SourceTypeArchive {
		t.Fatalf("expected source_type in config payload")
	}
	if _, ok := configPayload["hardware"]; !ok {
		t.Fatalf("expected legacy hardware field in config payload")
	}
	if _, ok := configPayload["specification"]; !ok {
		t.Fatalf("expected legacy specification field in config payload")
	}
}
