// Exercise 3: implement your own combineReducers (medium-hard)
//
// Per theory Part 1 section 5: combineReducers builds ONE root reducer
// out of many slice reducers, each owning one key of the overall state.
//
// TODO: implement miniCombineReducers(reducerMap) where reducerMap is
// { [sliceKey: string]: (sliceState: any, action: any) => any }.
// Return a single root reducer function (state, action) => newState that:
//   - for EACH key in reducerMap, calls that slice's reducer with
//     (state?.[key], action) — note state may be undefined on the very
//     first call, matching how Redux initializes state by calling every
//     reducer once with `undefined` so each can return its own default
//   - builds a new state object from all the slice results
//   - IMPORTANT optimization real combineReducers also does: if NONE of
//     the slice results differ (by reference, ===) from the previous
//     state's corresponding slice, return the ORIGINAL state object
//     unchanged (not a new object with identical contents) — this is
//     what lets things like useSelector's reference-equality checks
//     avoid false-positive "changed" signals when an action only
//     affected one slice

type SliceReducer<S = any, A = any> = (state: S | undefined, action: A) => S;
type ReducerMap = Record<string, SliceReducer>;

function miniCombineReducers(reducerMap: ReducerMap): (state: any, action: any) => any {
  // TODO: implement per the spec above
  return (state, action) => state;
}

export { miniCombineReducers };
export type { SliceReducer, ReducerMap };

// --- self-check: do not modify below this line ---
function counterReducer(state = { count: 0 }, action: { type: string }) {
  if (action.type === "INCREMENT") return { count: state.count + 1 };
  return state;
}

function todosReducer(state: { id: number }[] = [], action: { type: string; payload?: any }) {
  if (action.type === "todos/add") return [...state, { id: action.payload.id }];
  return state;
}

const rootReducer = miniCombineReducers({ counter: counterReducer, todos: todosReducer });

const checks: Array<[string, unknown, unknown]> = [];

// First call with undefined state — each slice reducer supplies its own default
const initial = rootReducer(undefined, { type: "@@INIT" });
checks.push(["initial state combines each slice's own default",
  JSON.stringify(initial), JSON.stringify({ counter: { count: 0 }, todos: [] })]);

const afterIncrement = rootReducer(initial, { type: "INCREMENT" });
checks.push(["dispatching INCREMENT updates only the counter slice's value",
  afterIncrement.counter.count, 1]);
checks.push(["todos slice is untouched (SAME reference) after an unrelated action",
  afterIncrement.todos === initial.todos, true]);
checks.push(["root state object itself is a NEW reference when something changed",
  afterIncrement !== initial, true]);

const afterUnknown = rootReducer(afterIncrement, { type: "TOTALLY_UNKNOWN" });
checks.push(["root state is the SAME reference when NO slice's result changed",
  afterUnknown === afterIncrement, true]);

const afterAddTodo = rootReducer(afterIncrement, { type: "todos/add", payload: { id: 1 } });
checks.push(["todos/add updates only the todos slice",
  afterAddTodo.todos.length, 1]);
checks.push(["counter slice untouched (SAME reference) after todos/add",
  afterAddTodo.counter === afterIncrement.counter, true]);

let passed = 0;
for (const [label, actual, expected] of checks) {
  const ok = JSON.stringify(actual) === JSON.stringify(expected);
  passed += ok ? 1 : 0;
  console.log(`${ok ? "PASS" : "FAIL"}: ${label} (got ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)})`);
}
console.log(passed === checks.length ? "PASS" : "FAIL");
