/**
 * Integration tests for project store synchronization with core models
 * Tests the critical data flow: Store Actions → Core Models → UI State Updates
 */
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { act } from '@testing-library/react';

// Import core classes
import { KGCore } from '../../../core/KGCore';
import { KGProject } from '../../../core/KGProject';
import { KGMidiTrack, type InstrumentType } from '../../../core/track/KGMidiTrack';
import { KGMidiRegion } from '../../../core/region/KGMidiRegion';
import { KGMidiNote } from '../../../core/midi/KGMidiNote';
import { KGMidiInput } from '../../../core/midi-input/KGMidiInput';

// Import store
import { useProjectStore } from '../../../stores/projectStore';

// Import test utilities and mocks
import '../../utils/setup-integration-tests';
import { mockAudioInterface } from '../../mocks/audio-interface';

describe('Project Store Synchronization Integration Tests', () => {
  let testProject: KGProject;
  
  beforeEach(async () => {
    // Create a real project for testing
    testProject = new KGProject('Sync Test Project');
    testProject.setBpm(120);
    testProject.setTimeSignature({ numerator: 4, denominator: 4 });
    testProject.setKeySignature('C major');
    testProject.setMaxBars(32);
    
    // Initialize KGCore
    const core = KGCore.instance();
    await core.initialize();
    core.setCurrentProject(testProject);
    
    // Reset store state
    const store = useProjectStore.getState();
    await store.loadProject(testProject);
  });
  
  afterEach(() => {
    vi.clearAllMocks();
  });

  describe('Project Properties Synchronization', () => {
    it('should sync BPM changes between store and core model', async () => {
      const { setBpm } = useProjectStore.getState();
      const newBpm = 140;
      
      // Execute store action
      act(() => {
        setBpm(newBpm);
      });
      
      // Verify core model was updated
      expect(testProject.getBpm()).toBe(newBpm);
      
      // Verify store state reflects change
      const storeState = useProjectStore.getState();
      expect(storeState.bpm).toBe(newBpm);
      
      // Verify CSS custom property was updated
      const cssValue = getComputedStyle(document.documentElement).getPropertyValue('--time-signature-numerator');
      expect(cssValue).toBeTruthy(); // CSS should be updated by store action
    });

    it('should sync time signature changes and update CSS properties', async () => {
      const { setTimeSignature } = useProjectStore.getState();
      const newTimeSignature = { numerator: 3, denominator: 4 };
      
      act(() => {
        setTimeSignature(newTimeSignature);
      });
      
      // Verify core model was updated
      expect(testProject.getTimeSignature()).toEqual(newTimeSignature);
      
      // Verify store state reflects change
      const storeState = useProjectStore.getState();
      expect(storeState.timeSignature).toEqual(newTimeSignature);
      
      // Verify CSS custom property was updated for UI calculations
      const cssValue = getComputedStyle(document.documentElement).getPropertyValue('--time-signature-numerator');
      expect(cssValue.trim()).toBe('3');
    });

    it('should sync max bars changes and update layout CSS', async () => {
      const { setMaxBars } = useProjectStore.getState();
      const newMaxBars = 64;
      
      act(() => {
        setMaxBars(newMaxBars);
      });
      
      // Verify core model was updated
      expect(testProject.getMaxBars()).toBe(newMaxBars);
      
      // Verify store state reflects change
      const storeState = useProjectStore.getState();
      expect(storeState.maxBars).toBe(newMaxBars);
      
      // Verify CSS custom property was updated for layout
      const cssValue = getComputedStyle(document.documentElement).getPropertyValue('--max-number-of-bars');
      expect(cssValue.trim()).toBe('64');
    });

    it('should sync key signature changes', async () => {
      const { setKeySignature } = useProjectStore.getState();
      const newKeySignature = 'G major';
      
      act(() => {
        setKeySignature(newKeySignature);
      });
      
      // Verify core model was updated
      expect(testProject.getKeySignature()).toBe(newKeySignature);
      
      // Verify store state reflects change
      const storeState = useProjectStore.getState();
      expect(storeState.keySignature).toBe(newKeySignature);
    });
  });

  describe('Track Management Synchronization', () => {
    it('should sync track addition with core model and audio interface', async () => {
      const { addTrack } = useProjectStore.getState();
      const initialTrackCount = testProject.getTracks().length;
      
      // Execute store action
      await act(async () => {
        await addTrack();
      });
      
      // Verify core model was updated
      expect(testProject.getTracks()).toHaveLength(initialTrackCount + 1);
      const newTrack = testProject.getTracks()[initialTrackCount] as KGMidiTrack;
      expect(newTrack).toBeInstanceOf(KGMidiTrack);
      expect(newTrack.getName()).toContain('Track');
      
      // Verify store state reflects change
      const storeState = useProjectStore.getState();
      expect(storeState.tracks).toHaveLength(initialTrackCount + 1);
      
      // Verify audio interface was notified
      expect(mockAudioInterface.createTrackBus).toHaveBeenCalled();
    });

    it('should sync track removal with core model and audio interface', async () => {
      // First add a track
      const { addTrack, removeTrack } = useProjectStore.getState();
      
      await act(async () => {
        await addTrack();
      });
      
      const trackCountAfterAdd = testProject.getTracks().length;
      const trackToRemove = testProject.getTracks()[trackCountAfterAdd - 1];
      
      // Remove the track
      await act(async () => {
        await removeTrack(trackCountAfterAdd - 1); // Remove last track
      });
      
      // Verify core model was updated
      expect(testProject.getTracks()).toHaveLength(trackCountAfterAdd - 1);
      
      // Verify store state reflects change
      const storeState = useProjectStore.getState();
      expect(storeState.tracks).toHaveLength(trackCountAfterAdd - 1);
      
      // Verify audio interface was notified
      expect(mockAudioInterface.removeTrackBus).toHaveBeenCalledWith(trackToRemove.getId());
    });

    it('should sync track instrument changes with audio interface', async () => {
      // Add a track first
      const { addTrack, setTrackInstrument } = useProjectStore.getState();
      
      await act(async () => {
        await addTrack();
      });
      
      const trackIndex = testProject.getTracks().length - 1;
      const track = testProject.getTracks()[trackIndex] as KGMidiTrack;
      const newInstrument: InstrumentType = 'electric_bass';
      
      // Change track instrument
      await act(async () => {
        await setTrackInstrument(trackIndex, newInstrument);
      });
      
      // Verify core model was updated
      expect(track.getInstrument()).toBe(newInstrument);
      
      // Verify store state reflects change
      const storeState = useProjectStore.getState();
      const storeTrack = storeState.tracks[trackIndex] as KGMidiTrack;
      expect(storeTrack.getInstrument()).toBe(newInstrument);
      
      // Verify audio interface was notified
      expect(mockAudioInterface.setTrackInstrument).toHaveBeenCalledWith(track.getId(), newInstrument);
    });

    it('should sync track reordering with core model', async () => {
      const { addTrack, reorderTracks } = useProjectStore.getState();
      
      // Add two tracks
      await act(async () => {
        await addTrack(); // Track at index 0
        await addTrack(); // Track at index 1
      });
      
      const track0Before = testProject.getTracks()[0];
      const track1Before = testProject.getTracks()[1];
      
      // Reorder tracks (move track 0 to position 1)
      act(() => {
        reorderTracks(0, 1);
      });
      
      // Verify core model track order changed
      const track0After = testProject.getTracks()[0];
      const track1After = testProject.getTracks()[1];
      
      expect(track0After.getId()).toBe(track1Before.getId());
      expect(track1After.getId()).toBe(track0Before.getId());
      
      // Verify store state reflects change
      const storeState = useProjectStore.getState();
      expect(storeState.tracks[0].getId()).toBe(track1Before.getId());
      expect(storeState.tracks[1].getId()).toBe(track0Before.getId());
    });
  });

  describe('Playback State Synchronization', () => {
    it('should sync playhead position with formatted time string', async () => {
      const { setPlayheadPosition } = useProjectStore.getState();
      const newPosition = 8.5; // beats
      
      act(() => {
        setPlayheadPosition(newPosition);
      });
      
      // Verify store state updated
      const storeState = useProjectStore.getState();
      expect(storeState.playheadPosition).toBe(newPosition);
      
      // Verify formatted time string was updated
      expect(storeState.currentTime).toBeDefined();
      expect(storeState.currentTime).toContain('|'); // Should contain BBB:B | mm:ss:mmm format
    });

    it('should sync playback state changes', async () => {
      const { startPlaying, stopPlaying } = useProjectStore.getState();
      
      // Start playing
      await act(async () => {
        await startPlaying();
      });
      
      // Verify store state updated
      let storeState = useProjectStore.getState();
      expect(storeState.isPlaying).toBe(true);
      
      // Verify audio interface was called
      expect(mockAudioInterface.startPlayback).toHaveBeenCalled();
      
      // Stop playing
      await act(async () => {
        await stopPlaying();
      });
      
      // Verify store state updated
      storeState = useProjectStore.getState();
      expect(storeState.isPlaying).toBe(false);
      
      // Verify audio interface was called
      expect(mockAudioInterface.stopPlayback).toHaveBeenCalled();
    });

    it('should start looped recording one bar before the loop start on the first pass', async () => {
      const testTrack = new KGMidiTrack('Recording Track', 0, 'acoustic_grand_piano');
      const testRegion = new KGMidiRegion('record-region', 'track-0', 0, 'Recording Region', 16, 16);
      testTrack.addRegion(testRegion);
      testProject.setTracks([testTrack]);
      testProject.setIsLooping(true);
      testProject.setLoopingRange([4, 7]);

      await act(async () => {
        await useProjectStore.getState().loadProject(testProject);
      });

      const core = KGCore.instance();
      const startPlayingSpy = vi.spyOn(core, 'startPlaying').mockResolvedValue(undefined);
      const recordingCallbacksSpy = vi.spyOn(KGMidiInput.instance(), 'setRecordingCallbacks');
      const { setActiveRegionId, setPlayheadPosition, startRecording } = useProjectStore.getState();

      act(() => {
        setActiveRegionId(testRegion.getId());
        setPlayheadPosition(18);
      });

      await act(async () => {
        await startRecording();
      });

      const storeState = useProjectStore.getState();
      expect(storeState.playheadPosition).toBe(12);
      expect(storeState.recordingOriginalPlayhead).toBe(18);
      expect(storeState.isRecording).toBe(true);
      expect(storeState.isPlaying).toBe(true);
      expect(recordingCallbacksSpy).toHaveBeenCalled();
      expect(startPlayingSpy).toHaveBeenCalledWith({ preserveLoopPreroll: true });
    });

    it('should commit recording when transport is stopped during recording', async () => {
      const testTrack = new KGMidiTrack('Recording Track', 0, 'acoustic_grand_piano');
      const testRegion = new KGMidiRegion('record-region', 'track-0', 0, 'Recording Region', 16, 16);
      testTrack.addRegion(testRegion);
      testProject.setTracks([testTrack]);

      await act(async () => {
        await useProjectStore.getState().loadProject(testProject);
      });

      const core = KGCore.instance();
      vi.spyOn(core, 'startPlaying').mockResolvedValue(undefined);
      const executeCommandSpy = vi.spyOn(core, 'executeCommand');
      const setRecordingCallbacksSpy = vi.spyOn(KGMidiInput.instance(), 'setRecordingCallbacks');
      vi.spyOn(mockAudioInterface, 'getTransportPosition').mockReturnValue(5);

      const { setActiveRegionId, setPlayheadPosition, startRecording, stopTransport } = useProjectStore.getState();

      act(() => {
        setActiveRegionId(testRegion.getId());
        setPlayheadPosition(4);
      });

      await act(async () => {
        await startRecording();
      });

      const noteOn = setRecordingCallbacksSpy.mock.calls.at(-1)?.[0];
      const noteOff = setRecordingCallbacksSpy.mock.calls.at(-1)?.[1];
      expect(noteOn).toBeTypeOf('function');
      expect(noteOff).toBeTypeOf('function');

      act(() => {
        noteOn?.(60, 96);
        noteOff?.(60);
      });

      await act(async () => {
        await stopTransport();
      });

      const storeState = useProjectStore.getState();
      expect(storeState.isRecording).toBe(false);
      expect(storeState.isPlaying).toBe(false);
      expect(storeState.recordingNotes).toHaveLength(0);
      expect(storeState.recordingPitchBends).toHaveLength(0);
      expect(executeCommandSpy).toHaveBeenCalled();
      expect(mockAudioInterface.stopPlayback).toHaveBeenCalled();
      expect(setRecordingCallbacksSpy).toHaveBeenLastCalledWith(null, null, null);
      expect(testRegion.getNotes()).toHaveLength(1);
      expect(testRegion.getNotes()[0].getVelocity()).toBe(96);
    });

    it('records pitch bends and skips consecutive duplicates', async () => {
      const testTrack = new KGMidiTrack('Recording Track', 0, 'acoustic_grand_piano');
      const testRegion = new KGMidiRegion('record-region', 'track-0', 0, 'Recording Region', 16, 16);
      testTrack.addRegion(testRegion);
      testProject.setTracks([testTrack]);

      await act(async () => {
        await useProjectStore.getState().loadProject(testProject);
      });

      const core = KGCore.instance();
      vi.spyOn(core, 'startPlaying').mockResolvedValue(undefined);
      vi.spyOn(mockAudioInterface, 'getTransportPosition')
        .mockReturnValueOnce(20)
        .mockReturnValueOnce(20.5)
        .mockReturnValueOnce(21);
      const setRecordingCallbacksSpy = vi.spyOn(KGMidiInput.instance(), 'setRecordingCallbacks');
      const { setActiveRegionId, setPlayheadPosition, startRecording, stopTransport } = useProjectStore.getState();

      act(() => {
        setActiveRegionId(testRegion.getId());
        setPlayheadPosition(18);
      });

      await act(async () => {
        await startRecording();
      });

      const onPitchBend = setRecordingCallbacksSpy.mock.calls.at(-1)?.[2];
      expect(onPitchBend).toBeTypeOf('function');

      act(() => {
        onPitchBend?.(8192);
        onPitchBend?.(8192);
        onPitchBend?.(12288);
      });

      await act(async () => {
        await stopTransport();
      });

      expect(testRegion.getPitchBends()).toHaveLength(2);
      expect(testRegion.getPitchBends().map(event => event.getValue())).toEqual([8192, 12288]);
    });

    it('should cut a held looped recording note at the loop end when note off arrives after wrap', async () => {
      const testTrack = new KGMidiTrack('Recording Track', 0, 'acoustic_grand_piano');
      const testRegion = new KGMidiRegion('record-region', 'track-0', 0, 'Recording Region', 16, 16);
      testTrack.addRegion(testRegion);
      testProject.setTracks([testTrack]);
      testProject.setIsLooping(true);
      testProject.setLoopingRange([4, 7]);

      await act(async () => {
        await useProjectStore.getState().loadProject(testProject);
      });

      const core = KGCore.instance();
      vi.spyOn(core, 'startPlaying').mockResolvedValue(undefined);
      vi.spyOn(mockAudioInterface, 'getTransportPosition')
        .mockReturnValueOnce(31.4)
        .mockReturnValueOnce(16.9);
      const setRecordingCallbacksSpy = vi.spyOn(KGMidiInput.instance(), 'setRecordingCallbacks');
      const { setActiveRegionId, setPlayheadPosition, startRecording, stopTransport } = useProjectStore.getState();

      act(() => {
        setActiveRegionId(testRegion.getId());
        setPlayheadPosition(18);
      });

      await act(async () => {
        await startRecording();
      });

      const noteOn = setRecordingCallbacksSpy.mock.calls.at(-1)?.[0];
      const noteOff = setRecordingCallbacksSpy.mock.calls.at(-1)?.[1];

      act(() => {
        noteOn?.(60, 72);
        noteOff?.(60);
      });

      await act(async () => {
        await stopTransport();
      });

      const notes = testRegion.getNotes();
      expect(notes).toHaveLength(1);
      expect(notes[0].getStartBeat()).toBeCloseTo(15);
      expect(notes[0].getEndBeat()).toBeCloseTo(16);
      expect(notes[0].getVelocity()).toBe(72);
    });

    it('should preserve velocity when finalizing a held recording note on stop', async () => {
      const testTrack = new KGMidiTrack('Recording Track', 0, 'acoustic_grand_piano');
      const testRegion = new KGMidiRegion('record-region', 'track-0', 0, 'Recording Region', 16, 16);
      testTrack.addRegion(testRegion);
      testProject.setTracks([testTrack]);

      await act(async () => {
        await useProjectStore.getState().loadProject(testProject);
      });

      const core = KGCore.instance();
      vi.spyOn(core, 'startPlaying').mockResolvedValue(undefined);
      vi.spyOn(mockAudioInterface, 'getTransportPosition')
        .mockReturnValueOnce(5)
        .mockReturnValueOnce(6.5);
      const setRecordingCallbacksSpy = vi.spyOn(KGMidiInput.instance(), 'setRecordingCallbacks');
      const { setActiveRegionId, setPlayheadPosition, startRecording, stopTransport } = useProjectStore.getState();

      act(() => {
        setActiveRegionId(testRegion.getId());
        setPlayheadPosition(4);
      });

      await act(async () => {
        await startRecording();
      });

      const noteOn = setRecordingCallbacksSpy.mock.calls.at(-1)?.[0];
      expect(noteOn).toBeTypeOf('function');

      act(() => {
        noteOn?.(60, 48);
      });

      await act(async () => {
        await stopTransport();
      });

      const notes = testRegion.getNotes();
      expect(notes).toHaveLength(1);
      expect(notes[0].getVelocity()).toBe(48);
    });
  });

  describe('Selection State Synchronization', () => {
    it('should sync selection state with core piano roll state', async () => {
      const { setActiveRegionId, syncSelectionFromCore } = useProjectStore.getState();
      
      // Add a track and region for testing
      const testTrack = new KGMidiTrack('Test Track', 0, 'acoustic_grand_piano');
      const testRegion = new KGMidiRegion('region-1', 'track-0', 0, 'Test Region', 0, 16);
      testTrack.addRegion(testRegion);
      testProject.setTracks([...testProject.getTracks(), testTrack]);
      
      // Set active region
      act(() => {
        setActiveRegionId(testRegion.getId());
      });
      
      // Verify store state updated
      let storeState = useProjectStore.getState();
      expect(storeState.activeRegionId).toBe(testRegion.getId());
      
      // Simulate core selection changes and sync
      act(() => {
        syncSelectionFromCore();
      });
      
      // Verify store selection state is synchronized
      storeState = useProjectStore.getState();
      expect(storeState.selectedNoteIds).toBeDefined();
      expect(storeState.selectedRegionIds).toBeDefined();
    });

    it('should clear all selections and sync state', async () => {
      const { clearAllSelections, setSelectedTrack } = useProjectStore.getState();
      
      // Set some initial selection state
      act(() => {
        setSelectedTrack('test-track-id');
      });
      
      // Verify selection was set
      let storeState = useProjectStore.getState();
      expect(storeState.selectedTrackId).toBe('test-track-id');
      
      // Clear all selections
      act(() => {
        clearAllSelections();
      });
      
      // Verify all selections were cleared
      storeState = useProjectStore.getState();
      expect(storeState.selectedTrackId).toBeNull();
      expect(storeState.selectedNoteIds).toEqual([]);
      expect(storeState.selectedRegionIds).toEqual([]);
    });
  });

  describe('Piano Roll State Integration', () => {
    it('should sync piano roll visibility and active region', async () => {
      const { setShowPianoRoll, setActiveRegionId } = useProjectStore.getState();
      
      // Add test region
      const testTrack = new KGMidiTrack('Test Track', 0, 'acoustic_grand_piano');
      const testRegion = new KGMidiRegion('region-2', 'track-0', 0, 'Test Region', 0, 16);
      testTrack.addRegion(testRegion);
      testProject.setTracks([...testProject.getTracks(), testTrack]);
      
      // Show piano roll with active region
      act(() => {
        setActiveRegionId(testRegion.getId());
        setShowPianoRoll(true);
      });
      
      // Verify store state updated
      const storeState = useProjectStore.getState();
      expect(storeState.showPianoRoll).toBe(true);
      expect(storeState.activeRegionId).toBe(testRegion.getId());
    });
  });

  describe('Error Handling and Edge Cases', () => {
    it('should handle invalid track operations gracefully', async () => {
      const { removeTrack } = useProjectStore.getState();
      const initialTrackCount = testProject.getTracks().length;
      
      // Try to remove non-existent track
      await act(async () => {
        await removeTrack(999); // Invalid index
      });
      
      // Verify project state unchanged
      expect(testProject.getTracks()).toHaveLength(initialTrackCount);
      
      // Verify store state unchanged
      const storeState = useProjectStore.getState();
      expect(storeState.tracks).toHaveLength(initialTrackCount);
    });

    it('should handle concurrent state updates correctly', async () => {
      const { setBpm, setMaxBars } = useProjectStore.getState();
      
      // Execute multiple state updates concurrently
      await act(async () => {
        setBpm(140);
        setMaxBars(64);
      });
      
      // Verify both updates were applied to core model
      expect(testProject.getBpm()).toBe(140);
      expect(testProject.getMaxBars()).toBe(64);
      
      // Verify store state is consistent
      const storeState = useProjectStore.getState();
      expect(storeState.bpm).toBe(140);
      expect(storeState.maxBars).toBe(64);
    });
  });

  describe('Project Loading Integration', () => {
    it('should completely sync store state when loading new project', async () => {
      const { loadProject } = useProjectStore.getState();
      
      // Create a new project with specific properties
      const newProject = new KGProject('New Loaded Project');
      newProject.setBpm(160);
      newProject.setTimeSignature({ numerator: 6, denominator: 8 });
      newProject.setKeySignature('D major');
      newProject.setMaxBars(48);
      
      // Add a track with region and notes
      const track = new KGMidiTrack('Loaded Track', 0, 'violin');
      const region = new KGMidiRegion('loaded-region', 'track-0', 0, 'Loaded Region', 0, 8);
      const note = new KGMidiNote('test-note', 0, 1, 64, 100);

      region.addNote(note);
      track.addRegion(region);
      newProject.setTracks([track]);
      
      // Load the new project
      await act(async () => {
        await loadProject(newProject);
      });
      
      // Verify store state completely matches new project
      const storeState = useProjectStore.getState();
      expect(storeState.projectName).toBe('New Loaded Project');
      expect(storeState.bpm).toBe(160);
      expect(storeState.timeSignature).toEqual({ numerator: 6, denominator: 8 });
      expect(storeState.keySignature).toBe('D major');
      expect(storeState.maxBars).toBe(48);
      expect(storeState.tracks).toHaveLength(1);
      
      // Verify core model is updated
      const core = KGCore.instance();
      expect(core.getCurrentProject()?.getName()).toBe('New Loaded Project');
      
      // Verify CSS properties were updated
      const timeSignatureCSS = getComputedStyle(document.documentElement).getPropertyValue('--time-signature-numerator');
      expect(timeSignatureCSS.trim()).toBe('6');
      
      const maxBarsCSS = getComputedStyle(document.documentElement).getPropertyValue('--max-number-of-bars');
      expect(maxBarsCSS.trim()).toBe('48');
    });
  });
});
