-- test_config_layer.lua
-- ============================================================================
-- Coverage for the HDG.Config layer:
--   1. Schema compile produces correct lookup maps (ByOption/ByKey/Defaults/ScopeBy)
--   2. Config:Get returns defaults for unset values, persisted values when set
--   3. Config:Set dispatches CONFIG_SET; reducer dual-writes to SV slot + mirror
--   4. Scope routing: Profile / Character / Account each land in the right SV slot
--   5. Profile lifecycle: NewProfile / SwitchProfile / DeleteProfile
--   6. SwitchProfile re-hydrates the mirror from the target profile
--   7. Legacy migration: HDG_DB.account.config -> HDG_DB.profiles.DEFAULT
--   8. Migration is idempotent (re-running Initialize doesn't double-copy)
--   9. Boot ordering: Initialize is safe to call after multiple SV resets
--
-- These are the contracts the rest of HDG relies on. A regression in any
-- of them surfaces as a silent stale-value or lost-config-change bug, so
-- the test is structural -- it asserts the data layer, not the UI.

dofile("wow_mock.lua")
dofile("../Core/HDGR_SchemeConstants.lua")
dofile("../Core/HDGR_Constants.lua")
dofile("../Core/HDGR_Format.lua")
dofile("../Core/HDGR_Paths.lua")
dofile("../Core/HDGR_Log.lua")
dofile("../Core/HDGR_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.lua")
dofile("../Core/HDGR_ConfigSchema.lua")
dofile("../Core/HDGR_Config.lua")
dofile("../UI/HDGR_Selectors.lua")

-- UnitGUID is needed by Config:_GetSourceForScope for Character routing.
-- wow_mock doesn't ship it; provide a stable test value.
_G.UnitGUID = _G.UnitGUID or function() return "Player-Test-AAAA" end

local function assertEq(actual, expected, label)
    if actual ~= expected then
        error(label .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual), 2)
    end
end
local function assertTrue(v, label)
    if not v then error(label, 2) end
end
local function reset()
    _G.HDG_DB = {}
    _G.HDG_DB_CURRENT_PROFILE = nil
    HDG.Store:LoadFromSavedVariables()
    HDG.Config:Initialize()
end

-- ===== 1. Schema compile =====================================================

assertTrue(type(HDG.ConfigSchema.ByOption) == "table",  "ByOption is a table")
assertTrue(type(HDG.ConfigSchema.ByKey)    == "table",  "ByKey is a table")
assertTrue(type(HDG.ConfigSchema.Defaults) == "table",  "Defaults is a table")
assertTrue(type(HDG.ConfigSchema.ScopeBy)  == "table",  "ScopeBy is a table")

assertEq(HDG.ConfigSchema.ByOption.DEBUG, "debug", "DEBUG -> 'debug'")
assertEq(HDG.ConfigSchema.ByOption.THEME, "scheme", "THEME -> 'scheme'")
assertEq(HDG.ConfigSchema.ScopeBy.scheme, HDG.Constants.ConfigScope.Profile,
    "THEME is Profile-scoped (shared across alts on the DEFAULT profile)")
assertEq(HDG.ConfigSchema.ScopeBy.decorPreviewBg, HDG.Constants.ConfigScope.Profile,
    "DECOR_PREVIEW_BG is Profile-scoped (shared across alts)")
assertEq(HDG.ConfigSchema.ScopeBy.debug,  HDG.Constants.ConfigScope.Profile,
    "DEBUG is Profile-scoped")
assertEq(HDG.ConfigSchema.ScopeBy.migrated_legacyConfig, HDG.Constants.ConfigScope.Account,
    "MIGRATED_LEGACY_CONFIG_TO_PROFILE is Account-scoped")
assertEq(HDG.ConfigSchema.Defaults.debug, false, "DEBUG default is false")

-- ===== 2. Config:Get returns defaults and persisted values ===================

reset()

-- DEFAULT profile is empty -> defaults apply (but _ImportDefaultsToActiveProfile
-- has populated profile slots so the read goes through profile, not fallback).
assertEq(HDG.Config:Get("DEBUG"), false, "Get DEBUG returns default (false)")
assertEq(HDG.Config:Get("WAYPOINT_PROVIDER"), "auto", "Get WAYPOINT_PROVIDER returns 'auto'")

-- Write via Set, read back.
HDG.Config:Set("DEBUG", true)
assertEq(HDG.Config:Get("DEBUG"), true, "Get DEBUG after Set returns true")
assertEq(HDG_DB.profiles.DEFAULT.debug, true, "SV slot HDG_DB.profiles.DEFAULT.debug set")
assertEq(HDG.Store:GetState().account.config.debug, true, "Mirror state.account.config.debug set")

-- ===== 3. Get unknown option errors at the API boundary ======================

local ok, err = pcall(function() HDG.Config:Get("BOGUS_OPTION_NAME") end)
assertTrue(not ok, "Get with unknown option errors")
assertTrue(tostring(err):find("unknown option") ~= nil,
    "error message mentions 'unknown option' (got: " .. tostring(err) .. ")")

-- ===== 4. Scope routing + cross-alt theme persistence ========================
-- THEME is Profile-scoped: it lands in the shared DEFAULT profile, so every
-- character on that profile sees the same theme. Regression for the tester
-- report "theme doesn't stick between alts" -- it was briefly Character-scoped
-- (stored per-GUID in characterSpecific[guid]), so each alt fell back to the
-- schema default instead of inheriting the chosen theme.

reset()

HDG.Config:Set("DEBUG", true)               -- Profile-scoped
HDG.Config:Set("THEME", "leather")          -- Profile-scoped (shared across alts)

assertEq(HDG_DB.profiles.DEFAULT.debug,  true,      "Profile-scoped DEBUG lands in profiles[DEFAULT]")
assertEq(HDG_DB.profiles.DEFAULT.scheme, "leather", "THEME lands in the shared profiles[DEFAULT], not per-character")

-- Mirror reflects both (selectors read state.account.config).
assertEq(HDG.Store:GetState().account.config.debug,  true,      "Mirror reflects DEBUG write")
assertEq(HDG.Store:GetState().account.config.scheme, "leather", "Mirror reflects THEME write")

-- Cross-alt: re-hydrate as a DIFFERENT character (new GUID) against the SAME
-- SavedVariables. Because THEME is Profile-scoped, the alt inherits the chosen
-- theme instead of resetting to the schema default.
local prevGUID = _G.UnitGUID
_G.UnitGUID = function() return "Player-Test-BBBB" end   -- a different alt
HDG.Config:Initialize()                                   -- re-hydrate from the shared DEFAULT profile
assertEq(HDG.Config:Get("THEME"), "leather",                   "Alt inherits the Profile theme (cross-alt persistence)")
assertEq(HDG.Store:GetState().account.config.scheme, "leather", "Alt mirror shows the shared theme")
_G.UnitGUID = prevGUID

-- ===== 5. Profile lifecycle ==================================================

reset()
assertEq(HDG.Config:GetActiveProfile(), "DEFAULT", "Active profile starts as DEFAULT")

HDG.Config:NewProfile("Hunter", false)   -- empty new profile
assertTrue(HDG_DB.profiles.Hunter ~= nil, "NewProfile creates the SV slot")
local names = HDG.Config:GetProfileNames()
assertEq(#names, 2, "GetProfileNames returns 2 (DEFAULT + Hunter)")

-- Clone from current.
HDG.Config:Set("DEBUG", true)
HDG.Config:NewProfile("Clone", true)     -- clone from active (DEFAULT)
assertEq(HDG_DB.profiles.Clone.debug, true, "NewProfile clone copies active profile fields")

-- ===== 6. SwitchProfile re-hydrates mirror ==================================

reset()
HDG.Config:Set("DEBUG", true)            -- DEFAULT.debug = true
HDG.Config:NewProfile("Quiet", false)    -- empty new profile
HDG.Config:Set("DEBUG", false)           -- explicit override won't happen if we switch first;
                                          -- but DEFAULT still has true.
-- Reset DEFAULT explicitly so switch tests cleanly.
reset()
HDG.Config:Set("DEBUG", true)
HDG.Config:NewProfile("Quiet", false)

HDG.Config:SwitchProfile("Quiet")
assertEq(HDG.Config:GetActiveProfile(), "Quiet", "After Switch, active is Quiet")
assertEq(HDG_DB_CURRENT_PROFILE, "Quiet",         "Per-character SV records Quiet")

-- Quiet is empty -> defaults apply -> mirror should reflect default (false).
assertEq(HDG.Config:Get("DEBUG"), false, "After Switch to empty profile, DEBUG falls back to default")
assertEq(HDG.Store:GetState().account.config.debug, false,
    "Mirror is re-hydrated from new profile after Switch")

-- Switch back, original value persists.
HDG.Config:SwitchProfile("DEFAULT")
assertEq(HDG.Config:Get("DEBUG"), true, "Switching back to DEFAULT restores DEBUG=true")

-- ===== 7. DeleteProfile guards ==============================================

reset()
HDG.Config:NewProfile("Tempo", false)

local okDel, errDel = pcall(function() HDG.Config:DeleteProfile("DEFAULT") end)
assertTrue(not okDel, "DeleteProfile DEFAULT is rejected")
assertTrue(tostring(errDel):find("Cannot delete the DEFAULT") ~= nil,
    "DeleteProfile DEFAULT error mentions DEFAULT")

HDG.Config:SwitchProfile("Tempo")
local okActive, errActive = pcall(function() HDG.Config:DeleteProfile("Tempo") end)
assertTrue(not okActive, "DeleteProfile of active profile is rejected")
assertTrue(tostring(errActive):find("active profile") ~= nil,
    "DeleteProfile of active profile error mentions 'active profile'")

HDG.Config:SwitchProfile("DEFAULT")
HDG.Config:DeleteProfile("Tempo")
assertEq(HDG_DB.profiles.Tempo, nil, "DeleteProfile removes the SV slot")

-- ===== 8. Legacy migration ==================================================

-- Simulate an existing user: HDG_DB.account.config has legacy values,
-- no profiles structure yet.
_G.HDG_DB = {
    account = {
        schemaVersion = 1,
        config = { debug = true, waypointProvider = "tomtom", scale = 1.25 },
    },
}
_G.HDG_DB_CURRENT_PROFILE = nil
HDG.Store:LoadFromSavedVariables()
HDG.Config:Initialize()

assertEq(HDG_DB.profiles.DEFAULT.debug, true,
    "Legacy migration copies debug into DEFAULT profile")
assertEq(HDG_DB.profiles.DEFAULT.waypointProvider, "tomtom",
    "Legacy migration copies waypointProvider into DEFAULT profile")
assertEq(HDG_DB.profiles.DEFAULT.scale, 1.25,
    "Legacy migration copies scale into DEFAULT profile")
assertEq(HDG_DB.migrated_legacyConfig, true,
    "MIGRATED_LEGACY_CONFIG_TO_PROFILE flag is set after migration")

-- ===== 9. Migration idempotency =============================================

-- Re-running Initialize must NOT re-copy or wipe data.
HDG_DB.profiles.DEFAULT.debug = false  -- user changed value post-migration
HDG.Config:Initialize()                -- re-run boot
assertEq(HDG_DB.profiles.DEFAULT.debug, false,
    "Re-running Initialize preserves user changes (does not re-overwrite from legacy)")

-- ===== 10. Schema <-> NewConfig drift guard =================================
-- Every Profile/Character schema key with a non-nil default MUST exist in
-- NewConfig (Store:GetDefaultConfig). EnsureConfig backfills account.config from
-- NewConfig, so a schema key missing there stays nil -> the Settings panel's proxy
-- pushes SetValue(nil) -> Blizzard asserts. Regressed twice (zoneScannerPopupShopping,
-- hideInCombat added to the schema but not NewConfig). Account-scoped migration flags
-- live outside account.config, so they're excluded.
do
    local Scope = HDG.Constants.ConfigScope
    local nc    = HDG.Store:GetDefaultConfig()
    for key, def in pairs(HDG.ConfigSchema.Defaults) do
        if HDG.ConfigSchema.ScopeBy[key] ~= Scope.Account then
            assertTrue(nc[key] ~= nil,
                "NewConfig() missing schema key '" .. key .. "' (default " .. tostring(def)
                .. ") -> Settings SetValue(nil) crash; add it to NewConfig()")
        end
    end
end

print("test_config_layer: PASS")
