package hooks

import (
	"context"
	"encoding/json"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	"go.uber.org/zap"

	"github.com/zzet/gortex/internal/daemon"
)

// fakeController implements daemon.Controller with stubbed SearchSymbols
// and no-op everything else. Lives in the hooks package so the e2e test
// can drive it through the real socket without dragging the hooks
// package into internal/daemon (which would create an import cycle).
type fakeController struct {
	hits []daemon.SymbolHit
}

func (f *fakeController) Track(_ context.Context, _ daemon.TrackParams) (json.RawMessage, error) {
	return json.RawMessage(`{}`), nil
}
func (f *fakeController) Untrack(_ context.Context, _ daemon.UntrackParams) (json.RawMessage, error) {
	return json.RawMessage(`{}`), nil
}
func (f *fakeController) Reload(_ context.Context) (json.RawMessage, error) {
	return json.RawMessage(`{}`), nil
}
func (f *fakeController) Status(_ context.Context) (daemon.StatusResponse, error) {
	return daemon.StatusResponse{}, nil
}
func (f *fakeController) Shutdown(_ context.Context) error { return nil }
func (f *fakeController) SearchSymbols(_ context.Context, _ daemon.SearchSymbolsParams) (daemon.SearchSymbolsResult, error) {
	return daemon.SearchSymbolsResult{Hits: f.hits}, nil
}

// startTestDaemon spins up a real daemon on a short-path unix socket and
// points GORTEX_DAEMON_SOCKET at it so daemon.Dial finds it.
func startTestDaemon(t *testing.T, ctrl daemon.Controller) {
	t.Helper()
	dir, err := os.MkdirTemp("/tmp", "gx-hook-e2e")
	if err != nil {
		t.Fatalf("mktemp: %v", err)
	}
	t.Cleanup(func() { _ = os.RemoveAll(dir) })

	socket := filepath.Join(dir, "s")
	t.Setenv("GORTEX_DAEMON_SOCKET", socket)
	t.Setenv("GORTEX_DAEMON_PIDFILE", filepath.Join(dir, "p"))

	srv := daemon.New(socket, "test-0.0.0", zap.NewNop())
	srv.Controller = ctrl
	if err := srv.Listen(); err != nil {
		t.Fatalf("listen: %v", err)
	}
	go func() { _ = srv.Serve() }()
	t.Cleanup(func() { _ = srv.Shutdown() })

	// Wait for the socket to actually accept.
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		c, err := net.DialTimeout("unix", socket, 100*time.Millisecond)
		if err == nil {
			_ = c.Close()
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatal("daemon socket never accepted")
}

func TestProbeViaDaemon_Hit_E2E(t *testing.T) {
	startTestDaemon(t, &fakeController{
		hits: []daemon.SymbolHit{
			{Name: "handleFoo", Kind: "function", FilePath: "x.go", Line: 7},
		},
	})
	hits, err := probeViaDaemon("handleFoo", 2*time.Second)
	if err != nil {
		t.Fatalf("probe error: %v", err)
	}
	if len(hits) != 1 || hits[0].Name != "handleFoo" || hits[0].FilePath != "x.go" || hits[0].Line != 7 {
		t.Errorf("unexpected hits: %+v", hits)
	}
}

func TestProbeViaDaemon_NoDaemon_ReturnsUnreachable(t *testing.T) {
	// Point at a path that has no listener.
	dir, err := os.MkdirTemp("/tmp", "gx-hook-empty")
	if err != nil {
		t.Fatalf("mktemp: %v", err)
	}
	t.Cleanup(func() { _ = os.RemoveAll(dir) })
	t.Setenv("GORTEX_DAEMON_SOCKET", filepath.Join(dir, "missing"))

	_, err = probeViaDaemon("handleFoo", 500*time.Millisecond)
	if err != errDaemonUnreachable {
		t.Errorf("expected errDaemonUnreachable, got %v", err)
	}
}
