package handlers

import (
	"encoding/json"
	"fmt"
	"net/http"
	"testing"

	"github.com/labstack/echo/v4"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"

	"github.com/treytartt/honeydue-api/internal/dto/requests"
	"github.com/treytartt/honeydue-api/internal/repositories"
	"github.com/treytartt/honeydue-api/internal/services"
	"github.com/treytartt/honeydue-api/internal/testutil"
)

func setupContractorHandler(t *testing.T) (*ContractorHandler, *echo.Echo, *gorm.DB) {
	db := testutil.SetupTestDB(t)
	contractorRepo := repositories.NewContractorRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	contractorService := services.NewContractorService(contractorRepo, residenceRepo)
	handler := NewContractorHandler(contractorService)
	e := testutil.SetupTestRouter()
	return handler, e, db
}

func TestContractorHandler_CreateContractor_MissingName_Returns400(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateContractor)

	t.Run("missing name returns 400 validation error", func(t *testing.T) {
		// Send request with no name (required field)
		req := requests.CreateContractorRequest{
			ResidenceID: &residence.ID,
		}

		w := testutil.MakeRequest(e, "POST", "/api/contractors/", req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusBadRequest)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Should contain structured validation error
		assert.Contains(t, response, "error")
		assert.Contains(t, response, "fields")

		fields := response["fields"].(map[string]interface{})
		assert.Contains(t, fields, "name", "validation error should reference the 'name' field")
	})

	t.Run("empty body returns 400 validation error", func(t *testing.T) {
		// Send completely empty body
		w := testutil.MakeRequest(e, "POST", "/api/contractors/", map[string]interface{}{}, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusBadRequest)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Contains(t, response, "error")
	})

	t.Run("valid contractor creation succeeds", func(t *testing.T) {
		req := requests.CreateContractorRequest{
			ResidenceID: &residence.ID,
			Name:        "John the Plumber",
		}

		w := testutil.MakeRequest(e, "POST", "/api/contractors/", req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusCreated)
	})
}

func TestContractorHandler_ListContractors_Error_NoRawErrorInResponse(t *testing.T) {
	_, e, db := setupContractorHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Create a handler with a broken service to simulate an internal error.
	// We do this by closing the underlying SQL connection, which will cause
	// the service to return an error on the next query.
	brokenDB := testutil.SetupTestDB(t)
	sqlDB, _ := brokenDB.DB()
	sqlDB.Close()

	brokenContractorRepo := repositories.NewContractorRepository(brokenDB)
	brokenResidenceRepo := repositories.NewResidenceRepository(brokenDB)
	brokenService := services.NewContractorService(brokenContractorRepo, brokenResidenceRepo)
	brokenHandler := NewContractorHandler(brokenService)

	authGroup := e.Group("/api/broken-contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/", brokenHandler.ListContractors)

	t.Run("internal error does not leak raw error message", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/broken-contractors/", nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusInternalServerError)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Should contain the generic error key, NOT a raw database error
		errorMsg, ok := response["error"].(string)
		require.True(t, ok, "response should have an 'error' string field")

		// Must not contain database-specific details
		assert.NotContains(t, errorMsg, "sql", "error message should not leak SQL details")
		assert.NotContains(t, errorMsg, "database", "error message should not leak database details")
		assert.NotContains(t, errorMsg, "closed", "error message should not leak connection state")
	})
}

func TestContractorHandler_CreateContractor_100Specialties_Returns400(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateContractor)

	t.Run("too many specialties rejected", func(t *testing.T) {
		// Create a slice with 100 specialty IDs (exceeds max=20)
		specialtyIDs := make([]uint, 100)
		for i := range specialtyIDs {
			specialtyIDs[i] = uint(i + 1)
		}

		req := requests.CreateContractorRequest{
			ResidenceID:  &residence.ID,
			Name:         "Over-specialized Contractor",
			SpecialtyIDs: specialtyIDs,
		}

		w := testutil.MakeRequest(e, "POST", "/api/contractors/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("20 specialties accepted", func(t *testing.T) {
		specialtyIDs := make([]uint, 20)
		for i := range specialtyIDs {
			specialtyIDs[i] = uint(i + 1)
		}

		req := requests.CreateContractorRequest{
			ResidenceID:  &residence.ID,
			Name:         "Multi-skilled Contractor",
			SpecialtyIDs: specialtyIDs,
		}

		w := testutil.MakeRequest(e, "POST", "/api/contractors/", req, "test-token")
		// Should pass validation (201 or success, not 400)
		assert.NotEqual(t, http.StatusBadRequest, w.Code, "20 specialties should pass validation")
	})

	t.Run("rating above 5 rejected", func(t *testing.T) {
		rating := 6.0
		req := requests.CreateContractorRequest{
			ResidenceID: &residence.ID,
			Name:        "Bad Rating Contractor",
			Rating:      &rating,
		}

		w := testutil.MakeRequest(e, "POST", "/api/contractors/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})
}

func TestContractorHandler_ListContractors(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Plumber Joe")
	testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Electrician Bob")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/", handler.ListContractors)

	t.Run("successful list", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/contractors/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response []map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.Len(t, response, 2)
	})

	t.Run("user with no contractors returns empty", func(t *testing.T) {
		otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")

		e2 := testutil.SetupTestRouter()
		authGroup2 := e2.Group("/api/contractors")
		authGroup2.Use(testutil.MockAuthMiddleware(otherUser))
		authGroup2.GET("/", handler.ListContractors)

		w := testutil.MakeRequest(e2, "GET", "/api/contractors/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response []map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.Len(t, response, 0)
	})
}

func TestContractorHandler_GetContractor(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Plumber Joe")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/:id/", handler.GetContractor)

	t.Run("successful get", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/contractors/%d/", contractor.ID), nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.Equal(t, "Plumber Joe", response["name"])
	})

	t.Run("not found returns 404", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/contractors/99999/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusNotFound)
	})

	t.Run("invalid id returns 400", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/contractors/invalid/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})
}

func TestContractorHandler_UpdateContractor(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Plumber Joe")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.PUT("/:id/", handler.UpdateContractor)

	t.Run("successful update", func(t *testing.T) {
		newName := "Plumber Joe Updated"
		req := requests.UpdateContractorRequest{
			Name: &newName,
		}

		w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/contractors/%d/", contractor.ID), req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.Equal(t, "Plumber Joe Updated", response["name"])
	})

	t.Run("invalid id returns 400", func(t *testing.T) {
		newName := "Updated"
		req := requests.UpdateContractorRequest{Name: &newName}
		w := testutil.MakeRequest(e, "PUT", "/api/contractors/invalid/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("not found returns 404", func(t *testing.T) {
		newName := "Updated"
		req := requests.UpdateContractorRequest{Name: &newName}
		w := testutil.MakeRequest(e, "PUT", "/api/contractors/99999/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusNotFound)
	})
}

func TestContractorHandler_DeleteContractor(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Plumber Joe")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.DELETE("/:id/", handler.DeleteContractor)

	t.Run("successful delete", func(t *testing.T) {
		w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/contractors/%d/", contractor.ID), nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.Contains(t, response, "message")
	})

	t.Run("invalid id returns 400", func(t *testing.T) {
		w := testutil.MakeRequest(e, "DELETE", "/api/contractors/invalid/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("not found returns 404", func(t *testing.T) {
		w := testutil.MakeRequest(e, "DELETE", "/api/contractors/99999/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusNotFound)
	})
}

func TestContractorHandler_ToggleFavorite(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Plumber Joe")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/:id/toggle-favorite/", handler.ToggleFavorite)

	t.Run("toggle favorite on", func(t *testing.T) {
		w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/contractors/%d/toggle-favorite/", contractor.ID), nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.Contains(t, response, "is_favorite")
	})

	t.Run("invalid id returns 400", func(t *testing.T) {
		w := testutil.MakeRequest(e, "POST", "/api/contractors/invalid/toggle-favorite/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("not found returns 404", func(t *testing.T) {
		w := testutil.MakeRequest(e, "POST", "/api/contractors/99999/toggle-favorite/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusNotFound)
	})
}

func TestContractorHandler_ListContractorsByResidence(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Plumber Joe")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/by-residence/:residence_id/", handler.ListContractorsByResidence)

	t.Run("successful list by residence", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/contractors/by-residence/%d/", residence.ID), nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response []map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.Len(t, response, 1)
	})

	t.Run("invalid residence id returns 400", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/contractors/by-residence/invalid/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})
}

func TestContractorHandler_GetSpecialties(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/specialties/", handler.GetSpecialties)

	t.Run("successful list specialties", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/contractors/specialties/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response []map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.Greater(t, len(response), 0)
	})
}

func TestContractorHandler_GetContractorTasks(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Plumber Joe")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/:id/tasks/", handler.GetContractorTasks)

	t.Run("successful get tasks", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/contractors/%d/tasks/", contractor.ID), nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusOK)
	})

	t.Run("invalid id returns 400", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/contractors/invalid/tasks/", nil, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})
}

func TestContractorHandler_CreateContractor_WithOptionalFields(t *testing.T) {
	handler, e, db := setupContractorHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/contractors")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateContractor)

	t.Run("creation with all optional fields", func(t *testing.T) {
		rating := 4.5
		isFavorite := true
		req := requests.CreateContractorRequest{
			ResidenceID: &residence.ID,
			Name:        "Full Contractor",
			Company:     "ABC Plumbing",
			Phone:       "555-1234",
			Email:       "contractor@test.com",
			Notes:       "Great work",
			Rating:      &rating,
			IsFavorite:  &isFavorite,
		}

		w := testutil.MakeRequest(e, "POST", "/api/contractors/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusCreated)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.Equal(t, "Full Contractor", response["name"])
		assert.Equal(t, "ABC Plumbing", response["company"])
	})
}
