package daemon

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/daemoncontrol"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/x/proc"
)

// Task8 will remove these obsolete startup-test injections from run_test.go.
// They stay test-only here so receipt discovery has no production fallback.
var (
	loadStatusConfig     = profile.Load
	discoverDaemonStatus = func() *Status { return nil }
)

type staticReceiptReader struct {
	receipt daemoncontrol.Receipt
	err     error
}

func (r staticReceiptReader) Read() (daemoncontrol.Receipt, error) {
	return r.receipt, r.err
}

func TestNewStatusMissingReceiptIsNotRunning(t *testing.T) {
	installStatusReceiptTestDependencies(t, daemoncontrol.Receipt{}, daemoncontrol.ErrReceiptMissing)
	probeStatusIdentity = func(context.Context, daemoncontrol.Receipt) (statusIdentity, error) {
		t.Fatal("identity probe must not run without a receipt")
		return statusIdentity{}, nil
	}

	status := NewStatus()

	require.False(t, status.Running)
	require.False(t, status.Healthy)
	require.Zero(t, status.PID)
}

func TestNewStatusFailsClosedForInvalidOrStaleReceipt(t *testing.T) {
	t.Run("invalid API version", func(t *testing.T) {
		receipt := validStatusReceipt(t)
		receipt.DesktopAPIVersion++
		installStatusReceiptTestDependencies(t, receipt, nil)
		lookupStatusProcess = func(int) (proc.ProcessInfo, bool) {
			t.Fatal("process lookup must not run for an invalid receipt")
			return proc.ProcessInfo{}, false
		}

		status := NewStatus()

		require.False(t, status.Running)
		require.False(t, status.Healthy)
	})

	t.Run("identity unavailable", func(t *testing.T) {
		receipt := validStatusReceipt(t)
		installStatusReceiptTestDependencies(t, receipt, nil)
		lookupStatusProcess = func(int) (proc.ProcessInfo, bool) {
			return proc.ProcessInfo{}, false
		}
		probeStatusIdentity = func(context.Context, daemoncontrol.Receipt) (statusIdentity, error) {
			t.Fatal("identity probe must not run when native process identity is unavailable")
			return statusIdentity{}, nil
		}

		status := NewStatus()

		require.False(t, status.Running)
		require.False(t, status.Healthy)
	})

	t.Run("stale process lifetime", func(t *testing.T) {
		receipt := validStatusReceipt(t)
		installStatusReceiptTestDependencies(t, receipt, nil)
		lookupStatusProcess = func(pid int) (proc.ProcessInfo, bool) {
			return proc.ProcessInfo{PID: pid, StartedAt: receipt.StartedAt.Add(time.Second)}, true
		}
		probeStatusIdentity = func(context.Context, daemoncontrol.Receipt) (statusIdentity, error) {
			t.Fatal("identity probe must not run for a stale process lifetime")
			return statusIdentity{}, nil
		}

		status := NewStatus()

		require.False(t, status.Running)
		require.False(t, status.Healthy)
	})

	t.Run("different native process lifetime", func(t *testing.T) {
		receipt := validStatusReceipt(t)
		receipt.StartID = "linux:boot-a:1234"
		installStatusReceiptTestDependencies(t, receipt, nil)
		lookupStatusProcess = func(pid int) (proc.ProcessInfo, bool) {
			return proc.ProcessInfo{
				PID:       pid,
				StartedAt: receipt.StartedAt,
				StartID:   "linux:boot-b:1234",
			}, true
		}

		status := NewStatus()

		require.False(t, status.Running)
		require.False(t, status.Healthy)
	})
}

func TestNewStatusNativeProcessIdentitySurvivesWallClockDrift(t *testing.T) {
	receipt := validStatusReceipt(t)
	receipt.StartID = "linux:boot-a:1234"
	installStatusReceiptTestDependencies(t, receipt, nil)
	lookupStatusProcess = func(pid int) (proc.ProcessInfo, bool) {
		return proc.ProcessInfo{
			PID:       pid,
			StartedAt: receipt.StartedAt.Add(-7 * time.Second),
			StartID:   receipt.StartID,
		}, true
	}
	probeStatusIdentity = func(context.Context, daemoncontrol.Receipt) (statusIdentity, error) {
		return statusIdentityFromReceipt(receipt), nil
	}

	status := NewStatus()

	require.True(t, status.Running)
	require.True(t, status.Healthy)
	assertStatusReceiptIdentity(t, status, receipt)
}

func TestNewStatusVerifiedProcessIsUnhealthyWhenIdentityEndpointIsAbsent(t *testing.T) {
	var sawToken string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		sawToken = r.Header.Get(controlTokenHeader)
		require.Equal(t, controlIdentityPath, r.URL.Path)
		require.Empty(t, r.URL.RawQuery)
		http.NotFound(w, r)
	}))
	defer server.Close()

	receipt := validStatusReceipt(t)
	receipt.BaseURL = server.URL
	installStatusReceiptTestDependencies(t, receipt, nil)

	status := NewStatus()

	require.True(t, status.Running)
	require.False(t, status.Healthy)
	require.Equal(t, receipt.ControlToken, sawToken)
	assertStatusReceiptIdentity(t, status, receipt)
}

func TestNewStatusHealthyOnlyForMatchingControlIdentity(t *testing.T) {
	receipt := validStatusReceipt(t)
	var requestCount int
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestCount++
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, controlIdentityPath, r.URL.Path)
		require.Empty(t, r.URL.RawQuery)
		require.Equal(t, receipt.ControlToken, r.Header.Get(controlTokenHeader))
		w.Header().Set("Content-Type", "application/json")
		require.NoError(t, json.NewEncoder(w).Encode(statusIdentityFromReceipt(receipt)))
	}))
	defer server.Close()
	receipt.BaseURL = server.URL
	installStatusReceiptTestDependencies(t, receipt, nil)

	status := NewStatus()

	require.True(t, status.Running)
	require.True(t, status.Healthy)
	require.Equal(t, 1, requestCount)
	assertStatusReceiptIdentity(t, status, receipt)
}

func TestNewStatusRejectsControlIdentityRedirectWithoutForwardingToken(t *testing.T) {
	var targetRequests int
	target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		targetRequests++
		require.Empty(t, r.Header.Get(controlTokenHeader))
		http.Error(w, "unexpected redirected request", http.StatusInternalServerError)
	}))
	defer target.Close()

	source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, target.URL+controlIdentityPath, http.StatusTemporaryRedirect)
	}))
	defer source.Close()

	receipt := validStatusReceipt(t)
	receipt.BaseURL = source.URL
	installStatusReceiptTestDependencies(t, receipt, nil)

	status := NewStatus()

	require.True(t, status.Running)
	require.False(t, status.Healthy)
	require.Zero(t, targetRequests)
}

func TestNewStatusRejectsTrailingControlIdentityJSON(t *testing.T) {
	receipt := validStatusReceipt(t)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		require.NoError(t, json.NewEncoder(w).Encode(statusIdentityFromReceipt(receipt)))
		_, err := w.Write([]byte(`{"unexpected":"second value"}`))
		require.NoError(t, err)
	}))
	defer server.Close()
	receipt.BaseURL = server.URL
	installStatusReceiptTestDependencies(t, receipt, nil)

	status := NewStatus()

	require.True(t, status.Running)
	require.False(t, status.Healthy)
}

func TestNewStatusRejectsMismatchedControlIdentity(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*statusIdentity)
	}{
		{"instance ID", func(identity *statusIdentity) { identity.InstanceID = "other-instance" }},
		{"API version", func(identity *statusIdentity) { identity.DesktopAPIVersion++ }},
		{"PID", func(identity *statusIdentity) { identity.PID++ }},
		{"process start", func(identity *statusIdentity) { identity.StartedAt = identity.StartedAt.Add(time.Second) }},
		{"base URL", func(identity *statusIdentity) { identity.BaseURL = "http://127.0.0.1:1" }},
		{"owner kind", func(identity *statusIdentity) { identity.OwnerKind = daemoncontrol.OwnerKindCLI }},
		{"draining", func(identity *statusIdentity) { identity.Draining = true }},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			receipt := validStatusReceipt(t)
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				identity := statusIdentityFromReceipt(receipt)
				tt.mutate(&identity)
				w.Header().Set("Content-Type", "application/json")
				require.NoError(t, json.NewEncoder(w).Encode(identity))
			}))
			defer server.Close()
			receipt.BaseURL = server.URL
			installStatusReceiptTestDependencies(t, receipt, nil)

			status := NewStatus()

			require.True(t, status.Running)
			require.False(t, status.Healthy)
			assertStatusReceiptIdentity(t, status, receipt)
		})
	}
}

func TestNewStatusReceiptReadErrorFailsClosed(t *testing.T) {
	installStatusReceiptTestDependencies(t, daemoncontrol.Receipt{}, errors.New("permission denied"))

	status := NewStatus()

	require.False(t, status.Running)
	require.False(t, status.Healthy)
}

func installStatusReceiptTestDependencies(t *testing.T, receipt daemoncontrol.Receipt, readErr error) {
	t.Helper()
	originalStore := defaultStatusReceiptStore
	originalLookup := lookupStatusProcess
	originalProbe := probeStatusIdentity
	t.Cleanup(func() {
		defaultStatusReceiptStore = originalStore
		lookupStatusProcess = originalLookup
		probeStatusIdentity = originalProbe
	})

	defaultStatusReceiptStore = func() statusReceiptReader {
		return staticReceiptReader{receipt: receipt, err: readErr}
	}
	lookupStatusProcess = func(pid int) (proc.ProcessInfo, bool) {
		return proc.ProcessInfo{
			PID:       pid,
			StartedAt: receipt.StartedAt,
			StartID:   receipt.StartID,
		}, true
	}
	probeStatusIdentity = probeControlIdentity
}

func validStatusReceipt(t *testing.T) daemoncontrol.Receipt {
	t.Helper()
	startedAt := time.Now().UTC().Truncate(time.Microsecond)
	return daemoncontrol.Receipt{
		SchemaVersion:     daemoncontrol.SchemaVersion,
		DesktopAPIVersion: daemoncontrol.DesktopAPIVersion,
		InstanceID:        "instance-a",
		PID:               os.Getpid(),
		StartedAt:         startedAt,
		BaseURL:           "http://127.0.0.1:8084",
		Owner: daemoncontrol.Owner{
			Kind: daemoncontrol.OwnerKindElectron,
			ID:   "owner-a",
			ProcessIdentity: daemoncontrol.ProcessIdentity{
				PID:       os.Getpid(),
				StartedAt: startedAt,
			},
		},
		ControlToken: "control-secret",
	}
}

func assertStatusReceiptIdentity(t *testing.T, status *Status, receipt daemoncontrol.Receipt) {
	t.Helper()
	require.Equal(t, receipt.InstanceID, status.InstanceID)
	require.Equal(t, receipt.DesktopAPIVersion, status.DesktopAPIVersion)
	require.Equal(t, receipt.PID, status.PID)
	require.True(t, receipt.StartedAt.Equal(status.StartTime))
	require.Equal(t, receipt.BaseURL, status.BaseURL)
	require.Equal(t, receipt.Owner.Kind, status.OwnerKind)
}
