package main

import (
	"bookhoard/internal/database"
	"bookhoard/internal/handlers"
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net/http"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type conflictTestEnv struct {
	setup     *TestServerSetup
	mediaID   string
	userID    pgtype.UUID
	mediaPGID pgtype.UUID
}

func setupConflictTest(t *testing.T) *conflictTestEnv {
	t.Helper()

	setup := setupTestServer(t)
	mediaID := createTestMediaItemID(t, setup)

	ctx := context.Background()
	user, err := setup.DB.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal")
	require.NoError(t, err)

	mediaUUID, err := uuid.Parse(mediaID)
	require.NoError(t, err)

	return &conflictTestEnv{
		setup:     setup,
		mediaID:   mediaID,
		userID:    user.ID,
		mediaPGID: pgtype.UUID{Bytes: [16]byte(mediaUUID), Valid: true},
	}
}

func createTestConflict(t *testing.T, env *conflictTestEnv, conflictData map[string]interface{}) database.SyncConflicts {
	t.Helper()
	ctx := context.Background()

	dataJSON, err := json.Marshal(conflictData)
	require.NoError(t, err)

	conflict, err := env.setup.DB.CreateSyncConflict(ctx, database.CreateSyncConflictParams{
		MediaItemID:  env.mediaPGID,
		UserID:       env.userID,
		ConflictType: "progress",
		ConflictData: dataJSON,
	})
	require.NoError(t, err)

	return conflict
}

func makeConflictData(koreaderPct, koboPct float64) map[string]interface{} {
	return map[string]interface{}{
		"koreader": map[string]interface{}{
			"source":    "koreader",
			"timestamp": time.Date(2026, 1, 30, 20, 10, 0, 0, time.UTC),
			"data": map[string]interface{}{
				"percentage": koreaderPct,
			},
		},
		"kobo": map[string]interface{}{
			"source":    "kobo",
			"timestamp": time.Date(2026, 1, 30, 20, 5, 0, 0, time.UTC),
			"data": map[string]interface{}{
				"percentage": koboPct,
			},
		},
	}
}

func TestConflictList_Empty(t *testing.T) {
	setup := setupTestServer(t)
	client := &http.Client{}

	req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts", nil)
	req.Header.Set("Authorization", "Bearer "+setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result handlers.ConflictListResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	assert.Equal(t, 0, result.Total)
	assert.Empty(t, result.Conflicts)
}

func TestConflictList_WithConflicts(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	createTestConflict(t, env, makeConflictData(0.45, 0.42))

	req, _ := http.NewRequest("GET", env.setup.Server.URL+"/api/conflicts?status=all", nil)
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result handlers.ConflictListResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	assert.GreaterOrEqual(t, result.Total, 1)
	require.NotEmpty(t, result.Conflicts)

	conflict := result.Conflicts[0]
	assert.Equal(t, "progress", conflict.ConflictType)
	assert.Equal(t, "unresolved", conflict.ResolutionStatus)
	assert.Contains(t, conflict.ConflictData, "koreader")
	assert.Contains(t, conflict.ConflictData, "kobo")
}

func TestConflictList_UnresolvedCount(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	createTestConflict(t, env, makeConflictData(0.45, 0.42))

	req, _ := http.NewRequest("GET", env.setup.Server.URL+"/api/conflicts", nil)
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	var result handlers.ConflictListResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	assert.GreaterOrEqual(t, result.Unresolved, 1)
}

func TestConflictGet_ByID(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	conflict := createTestConflict(t, env, makeConflictData(0.50, 0.30))
	conflictID := uuid.UUID(conflict.ID.Bytes).String()

	req, _ := http.NewRequest("GET", env.setup.Server.URL+"/api/conflicts/"+conflictID, nil)
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var detail handlers.ConflictDetailResponse
	err = json.NewDecoder(resp.Body).Decode(&detail)
	require.NoError(t, err)

	assert.Equal(t, conflictID, detail.ID)
	assert.Equal(t, env.mediaID, detail.MediaItemID)
	assert.Equal(t, "progress", detail.ConflictType)
	assert.Contains(t, detail.ConflictData, "koreader")
	assert.Contains(t, detail.ConflictData, "kobo")
}

func TestConflictGet_NotFound(t *testing.T) {
	setup := setupTestServer(t)
	client := &http.Client{}

	req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts/"+uuid.New().String(), nil)
	req.Header.Set("Authorization", "Bearer "+setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}

func TestConflictGet_InvalidID(t *testing.T) {
	setup := setupTestServer(t)
	client := &http.Client{}

	req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts/not-a-uuid", nil)
	req.Header.Set("Authorization", "Bearer "+setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}

func TestConflictResolve_ByKOReader(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	conflict := createTestConflict(t, env, makeConflictData(0.75, 0.30))
	conflictID := uuid.UUID(conflict.ID.Bytes).String()

	resolveReq := map[string]interface{}{
		"winner":                        "koreader",
		"manual_data":                   nil,
		"apply_to_all_future_conflicts": false,
		"reason":                        "More recent progress",
	}
	body, _ := json.Marshal(resolveReq)

	req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result handlers.ConflictResolveResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	assert.True(t, result.ConflictResolved)
	assert.Equal(t, map[string]bool{"progress": true, "annotations": false}, result.AppliedTo)
}

func TestConflictResolve_ByKobo(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	conflict := createTestConflict(t, env, makeConflictData(0.30, 0.75))
	conflictID := uuid.UUID(conflict.ID.Bytes).String()

	resolveReq := map[string]interface{}{
		"winner":                        "kobo",
		"apply_to_all_future_conflicts": false,
		"reason":                        "Higher progress",
	}
	body, _ := json.Marshal(resolveReq)

	req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result handlers.ConflictResolveResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	assert.True(t, result.ConflictResolved)
	assert.Equal(t, map[string]bool{"progress": true, "annotations": false}, result.AppliedTo)
}

func TestConflictResolve_WithManualData(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
	conflictID := uuid.UUID(conflict.ID.Bytes).String()

	resolveReq := map[string]interface{}{
		"winner": "manual",
		"manual_data": map[string]interface{}{
			"percentage": 0.43,
			"epubcfi":    "epubcfi(/6/4/2:20)",
			"chapter":    3,
		},
		"apply_to_all_future_conflicts": false,
		"reason":                        "Custom merged position",
	}
	body, _ := json.Marshal(resolveReq)

	req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result handlers.ConflictResolveResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(t, err)

	assert.True(t, result.ConflictResolved)
}

func TestConflictResolve_ManualWithoutData(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
	conflictID := uuid.UUID(conflict.ID.Bytes).String()

	resolveReq := map[string]interface{}{
		"winner":      "manual",
		"manual_data": nil,
	}
	body, _ := json.Marshal(resolveReq)

	req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}

func TestConflictResolve_AlreadyResolved(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	conflict := createTestConflict(t, env, makeConflictData(0.50, 0.30))
	conflictID := uuid.UUID(conflict.ID.Bytes).String()

	resolveReq := map[string]interface{}{
		"winner": "koreader",
		"reason": "First resolution",
	}
	body, _ := json.Marshal(resolveReq)

	req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	req2, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
	req2.Header.Set("Content-Type", "application/json")
	req2.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp2, err := client.Do(req2)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp2.Body)

	assert.Equal(t, http.StatusBadRequest, resp2.StatusCode)
}

func TestConflictResolve_InvalidWinner(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
	conflictID := uuid.UUID(conflict.ID.Bytes).String()

	resolveReq := map[string]interface{}{
		"winner": "nonexistent_source",
	}
	body, _ := json.Marshal(resolveReq)

	req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}

func TestConflictResolve_NotFound(t *testing.T) {
	setup := setupTestServer(t)
	client := &http.Client{}

	resolveReq := map[string]interface{}{
		"winner": "koreader",
	}
	body, _ := json.Marshal(resolveReq)

	req, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/"+uuid.New().String()+"/resolve", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}

func TestConflictDelete(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
	conflictID := uuid.UUID(conflict.ID.Bytes).String()

	req, _ := http.NewRequest("DELETE", env.setup.Server.URL+"/api/conflicts/"+conflictID, nil)
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusNoContent, resp.StatusCode)

	req2, _ := http.NewRequest("GET", env.setup.Server.URL+"/api/conflicts/"+conflictID, nil)
	req2.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp2, err := client.Do(req2)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp2.Body)

	assert.Equal(t, http.StatusNotFound, resp2.StatusCode)
}

func TestConflictDelete_NotFound(t *testing.T) {
	setup := setupTestServer(t)
	client := &http.Client{}

	req, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/conflicts/"+uuid.New().String(), nil)
	req.Header.Set("Authorization", "Bearer "+setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)

	assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}

func TestConflictDismissAllResolved(t *testing.T) {
	env := setupConflictTest(t)
	client := &http.Client{}

	conflict := createTestConflict(t, env, makeConflictData(0.50, 0.30))
	conflictID := uuid.UUID(conflict.ID.Bytes).String()

	resolveReq := map[string]interface{}{
		"winner": "koreader",
	}
	body, _ := json.Marshal(resolveReq)

	req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	req2, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/dismiss-all", nil)
	req2.Header.Set("Authorization", "Bearer "+env.setup.Token)

	resp2, err := client.Do(req2)
	require.NoError(t, err)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp2.Body)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result map[string]interface{}
	err = json.NewDecoder(resp2.Body).Decode(&result)
	require.NoError(t, err)

	deleted, ok := result["deleted"].(float64)
	assert.True(t, ok)
	assert.GreaterOrEqual(t, int(deleted), 1)
}

func TestConflictEndpoints_RequireAuth(t *testing.T) {
	setup := setupTestServer(t)
	client := &http.Client{}

	t.Run("list conflicts requires auth", func(t *testing.T) {
		req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts", nil)
		resp, err := client.Do(req)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(resp.Body)
		assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
	})

	t.Run("get conflict requires auth", func(t *testing.T) {
		req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts/"+uuid.New().String(), nil)
		resp, err := client.Do(req)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(resp.Body)
		assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
	})

	t.Run("resolve conflict requires auth", func(t *testing.T) {
		req, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/"+uuid.New().String()+"/resolve", bytes.NewBuffer([]byte(`{}`)))
		req.Header.Set("Content-Type", "application/json")
		resp, err := client.Do(req)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(resp.Body)
		assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
	})

	t.Run("delete conflict requires auth", func(t *testing.T) {
		req, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/conflicts/"+uuid.New().String(), nil)
		resp, err := client.Do(req)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(resp.Body)
		assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
	})

	t.Run("dismiss-all requires auth", func(t *testing.T) {
		req, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/dismiss-all", nil)
		resp, err := client.Do(req)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(resp.Body)
		assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
	})
}
