package service

import (
	"safesight-control/internal/config"
	"safesight-control/internal/models"
	"safesight-control/internal/storage"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"
)

func waitForTaskDone(t *testing.T, task *models.Task, timeout time.Duration) models.TaskStatus {
    t.Helper()
    deadline := time.Now().Add(timeout)
    for time.Now().Before(deadline) {
        task.Mu.RLock()
        st := task.Status
        task.Mu.RUnlock()
        if st == models.TaskSuccess || st == models.TaskFailed {
            return st
        }
        time.Sleep(10 * time.Millisecond)
    }
    t.Fatalf("timed out waiting for task to finish")
    return ""
}

func TestTaskService_CreateTask(t *testing.T) {
    cfg := &config.Config{
        Concurrency: 5,
    }
    // Mock registry
    agent := NewAgentClient(cfg)
    reg := NewRegistryService(cfg, agent)
    reg.UpdateDevice(&models.Device{
        DeviceID:  "dev1",
        IP:        "127.0.0.1",
        AgentPort: 9100,
        Online:    true,
    })
    svc := NewTaskService(cfg, agent, reg)

    task, err := svc.CreateTask("config_apply", []string{"dev1"}, map[string]string{"foo": "bar"})
    if err != nil {
        t.Fatalf("failed to create task: %v", err)
    }

    if task.ID == "" {
        t.Error("expected task ID to be set")
    }

    // Wait for task to finish or fail (since agent is nil, it will fail)
    time.Sleep(100 * time.Millisecond)

    task.Mu.RLock()
    defer task.Mu.RUnlock()

    if task.Devices["dev1"].Status == models.TaskPending {
        t.Error("expected task status to change from pending")
    }
}

func TestTaskService_Subscribe(t *testing.T) {
    cfg := &config.Config{
        Concurrency: 5,
    }
    svc := NewTaskService(cfg, NewAgentClient(cfg), NewRegistryService(cfg, NewAgentClient(cfg)))

    taskID := "test-task"
    svc.tasks[taskID] = models.NewTask(taskID, "test", []string{"dev1"}, nil)

    ch, cleanup := svc.Subscribe(taskID)
    defer cleanup()

    go func() {
        svc.updateDeviceStatus(taskID, "dev1", models.TaskRunning, 0.5, "")
    }()

    select {
    case update := <-ch:
        if update.DeviceID != "dev1" || update.Status != models.TaskRunning {
            t.Errorf("unexpected update: %+v", update)
        }
    case <-time.After(1 * time.Second):
        t.Error("timed out waiting for event")
    }
}

func TestTaskService_ConfigApply_UsesPayloadConfigField(t *testing.T) {
    var gotBody any
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPut {
            t.Fatalf("expected PUT, got %s", r.Method)
        }
        if r.URL.Path != "/v1/config" {
            t.Fatalf("expected path /v1/config, got %s", r.URL.Path)
        }
        _ = json.NewDecoder(r.Body).Decode(&gotBody)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"ok":true}`))
    }))
    defer server.Close()

    u, _ := url.Parse(server.URL)
    host, portStr, err := net.SplitHostPort(u.Host)
    if err != nil {
        t.Fatalf("SplitHostPort(%q): %v", u.Host, err)
    }
    port, _ := strconv.Atoi(portStr)

    cfg := &config.Config{Concurrency: 1}
    agent := NewAgentClient(cfg)
    reg := NewRegistryService(cfg, agent)
    reg.UpdateDevice(&models.Device{DeviceID: "dev1", IP: host, AgentPort: port, Online: true})
    svc := NewTaskService(cfg, agent, reg)

    payload := map[string]any{"config": map[string]any{"a": 1}}
    task, err := svc.CreateTask("config_apply", []string{"dev1"}, payload)
    if err != nil {
        t.Fatalf("failed to create task: %v", err)
    }
    st := waitForTaskDone(t, task, 2*time.Second)
    if st != models.TaskSuccess {
        t.Fatalf("expected task success, got %s", st)
    }

    m, ok := gotBody.(map[string]any)
    if !ok || m["a"].(float64) != 1 {
        t.Fatalf("expected body {a:1}, got %#v", gotBody)
    }
}

func TestTaskService_MediaStart_IgnoresInvalidConfigShape(t *testing.T) {
    var bodyBytes []byte
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            t.Fatalf("expected POST, got %s", r.Method)
        }
        if r.URL.Path != "/v1/media-server/start" {
            t.Fatalf("expected path /v1/media-server/start, got %s", r.URL.Path)
        }
        bodyBytes, _ = io.ReadAll(r.Body)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"ok":true}`))
    }))
    defer server.Close()

    u, _ := url.Parse(server.URL)
    host, portStr, err := net.SplitHostPort(u.Host)
    if err != nil {
        t.Fatalf("SplitHostPort(%q): %v", u.Host, err)
    }
    port, _ := strconv.Atoi(portStr)

    cfg := &config.Config{Concurrency: 1}
    agent := NewAgentClient(cfg)
    reg := NewRegistryService(cfg, agent)
    reg.UpdateDevice(&models.Device{DeviceID: "dev1", IP: host, AgentPort: port, Online: true})
    svc := NewTaskService(cfg, agent, reg)

    // UI default payload_json is {"config":{}}; this should be ignored for media_start.
    payload := map[string]any{"config": map[string]any{}}
    task, err := svc.CreateTask("media_start", []string{"dev1"}, payload)
    if err != nil {
        t.Fatalf("failed to create task: %v", err)
    }
    st := waitForTaskDone(t, task, 2*time.Second)
    if st != models.TaskSuccess {
        t.Fatalf("expected task success, got %s", st)
    }
    if len(bodyBytes) != 0 {
        t.Fatalf("expected empty body, got %q", string(bodyBytes))
    }
}

func TestTaskService_ConfigApplyPersistsDeviceConfigStateAndAudit(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"ok":true}`))
    }))
    defer server.Close()

    u, _ := url.Parse(server.URL)
    host, portStr, err := net.SplitHostPort(u.Host)
    if err != nil {
        t.Fatalf("SplitHostPort(%q): %v", u.Host, err)
    }
    port, _ := strconv.Atoi(portStr)

    cfg := &config.Config{Concurrency: 1}
    agent := NewAgentClient(cfg)
    reg := NewRegistryService(cfg, agent)
    reg.UpdateDevice(&models.Device{DeviceID: "dev1", IP: host, AgentPort: port, Online: true})
    store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
    if err != nil {
        t.Fatalf("OpenSQLite: %v", err)
    }
    defer store.Close()

    svc := NewTaskService(cfg, agent, reg)
    svc.SetDeviceConfigStateRepo(storage.NewDeviceConfigStateRepo(store.DB()))
    svc.SetAuditLogRepo(storage.NewAuditLogsRepo(store.DB()))

    payload := map[string]any{
        "config": map[string]any{
            "metadata": map[string]any{
                "template":       "helmet",
                "profile":        "gate_a",
                "overlays":       []any{"night_relaxed"},
                "config_id":      "cfg-001",
                "config_version": "20260427.1",
            },
        },
    }
    task, err := svc.CreateTask("config_apply", []string{"dev1"}, payload)
    if err != nil {
        t.Fatalf("CreateTask: %v", err)
    }
    if st := waitForTaskDone(t, task, 2*time.Second); st != models.TaskSuccess {
        t.Fatalf("expected task success, got %s", st)
    }

    state, err := storage.NewDeviceConfigStateRepo(store.DB()).Get("dev1")
    if err != nil {
        t.Fatalf("Get state: %v", err)
    }
    if state == nil || state.ProfileName != "gate_a" || state.ConfigID != "cfg-001" || state.LastAppliedTaskID != task.ID {
        t.Fatalf("unexpected state: %#v", state)
    }

    logs, err := storage.NewAuditLogsRepo(store.DB()).List()
    if err != nil {
        t.Fatalf("List audit logs: %v", err)
    }
    if len(logs) == 0 || logs[0].Action != "config_apply" || logs[0].TargetID != "dev1" {
        t.Fatalf("unexpected audit logs: %#v", logs)
    }
}

func TestTaskService_ModelSyncAllUploadsStandardModels(t *testing.T) {
    var uploads []string
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPut {
            t.Fatalf("expected PUT, got %s", r.Method)
        }
        uploads = append(uploads, r.URL.Path)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"ok":true}`))
    }))
    defer server.Close()

    u, _ := url.Parse(server.URL)
    host, portStr, err := net.SplitHostPort(u.Host)
    if err != nil {
        t.Fatalf("SplitHostPort(%q): %v", u.Host, err)
    }
    port, _ := strconv.Atoi(portStr)

    cfg := &config.Config{Concurrency: 1}
    agent := NewAgentClient(cfg)
    reg := NewRegistryService(cfg, agent)
    reg.UpdateDevice(&models.Device{DeviceID: "dev1", IP: host, AgentPort: port, Online: true})

    store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
    if err != nil {
        t.Fatalf("OpenSQLite: %v", err)
    }
    defer store.Close()
    repo := storage.NewModelsRepo(store.DB())
    modelDir := t.TempDir()
    body := []byte("model-a")
    sum := sha256SumHex(body)
    fileName := "face_det_scrfd_500m_640_rk3588.rknn"
    if err := os.WriteFile(filepath.Join(modelDir, fileName), body, 0o644); err != nil {
        t.Fatalf("WriteFile: %v", err)
    }
    if err := repo.Save(storage.StandardModelRecord{
        Name:     "face_det_scrfd_500m_640_rk3588",
        FileName: fileName,
        Version:  "v1.0.0",
        SHA256:   sum,
    }); err != nil {
        t.Fatalf("Save model: %v", err)
    }

    svc := NewTaskService(cfg, agent, reg)
    svc.SetStandardModels(repo, modelDir)

    task, err := svc.CreateTask("model_sync_all", []string{"dev1"}, map[string]any{})
    if err != nil {
        t.Fatalf("CreateTask: %v", err)
    }
    if st := waitForTaskDone(t, task, 2*time.Second); st != models.TaskSuccess {
        t.Fatalf("expected task success, got %s", st)
    }
    if len(uploads) != 1 || !strings.Contains(uploads[0], "/v1/models/face_det_scrfd_500m_640_rk3588") {
        t.Fatalf("unexpected uploads: %#v", uploads)
    }
}

func sha256SumHex(body []byte) string {
    sum := sha256.Sum256(body)
    return hex.EncodeToString(sum[:])
}
