/**
 * Unit Tests for Beta Channel Implementation (Phase 3)
 *
 * Tests the beta channel functionality added in Phase 3:
 * - Beta channel updates in update-checker.ts
 * - fetchVersionFromNpmTag function for 'dev' tag
 * - checkForUpdates with targetTag parameter
 * - Direct install beta rejection
 * - Beta stability warning display
 * - handleCheckFailed with targetTag
 */

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const https = require('https');
const os = require('os');

describe('Beta Channel Implementation (Phase 3)', function () {
  let updateCheckerModule;
  let originalFsExistsSync;
  let originalFsReadFileSync;
  let originalFsWriteFileSync;
  let originalFsMkdirSync;
  let originalHttpsGet;
  let mockFileSystem = {};
  let httpsRequests = [];
  let cacheFilePath;
  let cacheDirPath;

  beforeAll(async function () {
    // Note: Build is handled by CI before tests run (bun run build:all)
    // Import the built module
    updateCheckerModule = await import('../../../dist/utils/update-checker.js');
  });

  beforeEach(function () {
    // Reset mocks
    mockFileSystem = {};
    httpsRequests = [];
    cacheFilePath = undefined;
    cacheDirPath = undefined;

    // Store original functions
    originalFsExistsSync = fs.existsSync;
    originalFsReadFileSync = fs.readFileSync;
    originalFsWriteFileSync = fs.writeFileSync;
    originalFsMkdirSync = fs.mkdirSync;
    originalHttpsGet = https.get;

    // Mock fs.existsSync
    fs.existsSync = (filePath) => {
      const key = String(filePath);
      if (key.endsWith('/update-check.json') || key.endsWith('\\update-check.json')) {
        cacheFilePath = key;
        cacheDirPath = path.dirname(key);
      }
      if (key.endsWith('/cache') || key.endsWith('\\cache')) {
        cacheDirPath = key;
      }
      return mockFileSystem[key] !== undefined;
    };

    // Mock fs.readFileSync
    fs.readFileSync = (filePath, encoding) => {
      const key = String(filePath);
      if (mockFileSystem[key]) {
        return mockFileSystem[key];
      }
      throw new Error(`ENOENT: no such file or directory, open '${filePath}'`);
    };

    // Mock fs.writeFileSync
    fs.writeFileSync = (filePath, data, encoding) => {
      const key = String(filePath);
      if (key.endsWith('/update-check.json') || key.endsWith('\\update-check.json')) {
        cacheFilePath = key;
        cacheDirPath = path.dirname(key);
      }
      mockFileSystem[key] = data;
    };

    // Mock fs.mkdirSync
    fs.mkdirSync = (dirPath, options) => {
      const key = String(dirPath);
      if (key.endsWith('/cache') || key.endsWith('\\cache')) {
        cacheDirPath = key;
      }
      // Just mark as created for testing
      mockFileSystem[key] = 'directory';
    };

    // Mock https.get
    https.get = (url, options, callback) => {
      httpsRequests.push({ url, options: options || {} });

      // Create mock response object
      const mockRes = {
        statusCode: 200,
        on: function(event, handler) {
          if (event === 'data') {
            // Simulate receiving data
            setTimeout(() => {
              if (url.includes('/dev')) {
                handler('{"version":"5.5.0"}'); // Use a higher version for dev
              } else if (url.includes('/latest')) {
                handler('{"version":"5.4.1"}');
              }
            }, 0);
          } else if (event === 'end') {
            setTimeout(handler, 10);
          }
        }
      };

      // Call callback with mock response
      if (callback) {
        setTimeout(() => callback(mockRes), 0);
      }

      // Return mock request object
      return {
        on: function(event, handler) {
          if (event === 'error') {
            // Don't call error handler by default
          } else if (event === 'timeout') {
            // Don't call timeout handler by default
          }
        },
        destroy: function() {
          // Mock destroy method
        }
      };
    };

    // Force path discovery from module internals to avoid hardcoded ~/.ccs assumptions.
    updateCheckerModule.readCache();
  });

  afterEach(function () {
    // Restore original functions
    fs.existsSync = originalFsExistsSync;
    fs.readFileSync = originalFsReadFileSync;
    fs.writeFileSync = originalFsWriteFileSync;
    fs.mkdirSync = originalFsMkdirSync;
    https.get = originalHttpsGet;
  });

  describe('fetchVersionFromNpmTag function (internal)', function () {
    it('should request from correct npm registry URL for latest tag', async function () {
      // Call checkForUpdates which internally uses fetchVersionFromNpmTag
      const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');

      // Should have made request to correct URL
      const latestRequest = httpsRequests.find(req =>
        req.url === 'https://registry.npmjs.org/@kaitranntt/ccs/latest'
      );
      assert(latestRequest, 'should request latest tag from npm registry');

      // Should return version
      assert.strictEqual(result.status, 'update_available');
      assert.strictEqual(result.latest, '5.4.1');
    });

    it('should request from correct npm registry URL for dev tag', async function () {
      // Call checkForUpdates which internally uses fetchVersionFromNpmTag
      const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'dev');

      // Should have made request to correct URL
      const devRequest = httpsRequests.find(req =>
        req.url === 'https://registry.npmjs.org/@kaitranntt/ccs/dev'
      );
      assert(devRequest, 'should request dev tag from npm registry');

      // Should return version
      assert.strictEqual(result.status, 'update_available');
      assert.strictEqual(result.latest, '5.5.0');
    });

    it('should handle network errors gracefully', async function () {
      // Mock https.get to simulate network error
      https.get = (url, options, callback) => {
        const req = {
          on: function(event, handler) {
            if (event === 'error') {
              setTimeout(() => handler(new Error('Network error')), 0);
            }
          },
          destroy: function() {}
        };
        return req;
      };

      const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
      assert.strictEqual(result.status, 'check_failed');
      assert.strictEqual(result.reason, 'npm_registry_error');
    });

    it('should handle timeout errors gracefully', async function () {
      // Mock https.get to simulate timeout
      https.get = (url, options, callback) => {
        const mockRes = {
          statusCode: 200,
          on: function(event, handler) {
            // Don't send any data to trigger timeout
          }
        };

        if (callback) {
          setTimeout(() => callback(mockRes), 0);
        }

        return {
          on: function(event, handler) {
            if (event === 'timeout') {
              setTimeout(() => {
                handler();
                this.destroy();
              }, 100);
            }
          },
          destroy: function() {}
        };
      };

      const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
      assert.strictEqual(result.status, 'check_failed');
      assert.strictEqual(result.reason, 'npm_registry_error');
    });

    it('should handle invalid JSON response', async function () {
      // Mock https.get to return invalid JSON
      https.get = (url, options, callback) => {
        const mockRes = {
          statusCode: 200,
          on: function(event, handler) {
            if (event === 'data') {
              setTimeout(() => handler('invalid json'), 0);
            } else if (event === 'end') {
              setTimeout(handler, 10);
            }
          }
        };

        if (callback) {
          setTimeout(() => callback(mockRes), 0);
        }

        return {
          on: function() {},
          destroy: function() {}
        };
      };

      const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
      assert.strictEqual(result.status, 'check_failed');
      assert.strictEqual(result.reason, 'npm_registry_error');
    });

    it('should handle non-200 status codes', async function () {
      // Mock https.get to return 404
      https.get = (url, options, callback) => {
        const mockRes = {
          statusCode: 404,
          on: function(event, handler) {
            if (event === 'end') {
              setTimeout(handler, 0);
            }
          }
        };

        if (callback) {
          setTimeout(() => callback(mockRes), 0);
        }

        return {
          on: function() {},
          destroy: function() {}
        };
      };

      const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
      assert.strictEqual(result.status, 'check_failed');
      assert.strictEqual(result.reason, 'npm_registry_error');
    });
  });

  describe('checkForUpdates with targetTag parameter', function () {
    function getCacheFilePath() {
      return cacheFilePath || path.join(os.homedir(), '.ccs', 'cache', 'update-check.json');
    }

    beforeEach(function () {
      // Set up a fresh cache
      const cacheData = {
        last_check: 0,
        latest_version: null,
        dismissed_version: null
      };
      mockFileSystem[getCacheFilePath()] = JSON.stringify(cacheData);
    });

    it('should use latest tag when targetTag is "latest"', async function () {
      const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');

      // Should request latest tag
      const latestRequest = httpsRequests.find(req =>
        req.url.includes('/latest')
      );
      assert(latestRequest, 'should request latest tag');

      // Should return update available
      assert.strictEqual(result.status, 'update_available');
      assert.strictEqual(result.latest, '5.4.1');
      assert.strictEqual(result.current, '5.4.0');
    });

    it('should use dev tag when targetTag is "dev"', async function () {
      const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'dev');

      // Should request dev tag
      const devRequest = httpsRequests.find(req =>
        req.url.includes('/dev')
      );
      assert(devRequest, 'should request dev tag');

      // Should return update available (dev version newer)
      assert.strictEqual(result.status, 'update_available');
      assert.strictEqual(result.latest, '5.5.0');
      assert.strictEqual(result.current, '5.4.0');
    });

    it('should reject beta for direct install', async function () {
      const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'direct', 'dev');

      // Should return check_failed for beta on direct install
      assert.strictEqual(result.status, 'check_failed');
      assert.strictEqual(result.reason, 'beta_not_supported');
      assert.strictEqual(result.message, '--beta requires npm installation method');

      // Should not make any HTTP requests
      assert.strictEqual(httpsRequests.length, 0, 'should not make HTTP requests');
    });

    it('should allow beta for npm install', async function () {
      const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'dev');

      // Should NOT return check_failed for npm
      assert.notStrictEqual(result.status, 'check_failed');

      // Should make HTTP request
      assert(httpsRequests.length > 0, 'should make HTTP requests');
    });

    it('should use different fetch error for npm vs direct', async function () {
      // Mock https.get to fail
      https.get = (url, options, callback) => {
        const req = {
          on: function(event, handler) {
            if (event === 'error') {
              setTimeout(() => handler(new Error('Network error')), 0);
            }
          },
          destroy: function() {}
        };
        return req;
      };

      // Test npm install
      const npmResult = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'latest');
      assert.strictEqual(npmResult.status, 'check_failed');
      assert.strictEqual(npmResult.reason, 'npm_registry_error');

      // Reset requests
      httpsRequests = [];

      // Test direct install
      const directResult = await updateCheckerModule.checkForUpdates('5.4.1', true, 'direct', 'latest');
      assert.strictEqual(directResult.status, 'check_failed');
      assert.strictEqual(directResult.reason, 'github_api_error');
    });

    it('should update cache with correct version from target tag', async function () {
      await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'dev');

      // Check cache was updated with dev version
      const cachePath = getCacheFilePath();
      const cacheData = JSON.parse(mockFileSystem[cachePath]);

      // Dev versions are now stored in dev_version field (not latest_version)
      assert.strictEqual(cacheData.dev_version, '5.5.0');
      assert(cacheData.last_check > 0, 'should update timestamp');
    });

    it('should use cached result when within interval', async function () {
      // Set up cache with recent check - use dev_version for dev channel
      const cacheData = {
        last_check: Date.now() - 1000, // 1 second ago
        latest_version: null,
        dev_version: '5.5.0',
        dismissed_version: null
      };
      mockFileSystem[getCacheFilePath()] = JSON.stringify(cacheData);

      // Call with force=false to use cache
      const result = await updateCheckerModule.checkForUpdates('5.4.0', false, 'npm', 'dev');

      // Should not make HTTP requests
      assert.strictEqual(httpsRequests.length, 0, 'should use cached result');

      // Should return cached update
      assert.strictEqual(result.status, 'update_available');
      assert.strictEqual(result.latest, '5.5.0');
    });
  });

  describe('Version comparison with dev versions', function () {
    it('should correctly compare dev version as newer', async function () {
      const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'dev');

      // 5.5.0 should be newer than 5.4.0
      assert.strictEqual(result.status, 'update_available');
      assert.strictEqual(result.latest, '5.5.0');
    });

    it('should correctly handle same dev version', async function () {
      // Mock same version response
      https.get = (url, options, callback) => {
        const mockRes = {
          statusCode: 200,
          on: function(event, handler) {
            if (event === 'data') {
              setTimeout(() => handler('{"version":"5.4.1"}'), 0);
            } else if (event === 'end') {
              setTimeout(handler, 10);
            }
          }
        };

        if (callback) {
          setTimeout(() => callback(mockRes), 0);
        }

        return {
          on: function() {},
          destroy: function() {}
        };
      };

      const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'latest');

      // Same version should not trigger update
      assert.strictEqual(result.status, 'no_update');
      assert.strictEqual(result.reason, 'latest');
    });

    it('should correctly handle older dev version', async function () {
      // Mock older version response
      https.get = (url, options, callback) => {
        const mockRes = {
          statusCode: 200,
          on: function(event, handler) {
            if (event === 'data') {
              setTimeout(() => handler('{"version":"5.4.0"}'), 0);
            } else if (event === 'end') {
              setTimeout(handler, 10);
            }
          }
        };

        if (callback) {
          setTimeout(() => callback(mockRes), 0);
        }

        return {
          on: function() {},
          destroy: function() {}
        };
      };

      const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'dev');

      // Older version should not trigger update
      assert.strictEqual(result.status, 'no_update');
      assert.strictEqual(result.reason, 'latest');
    });
  });

  describe('Cache functionality', function () {
    function getCacheFilePath() {
      return cacheFilePath || path.join(os.homedir(), '.ccs', 'cache', 'update-check.json');
    }

    function getCacheDirPath() {
      return cacheDirPath || path.dirname(getCacheFilePath());
    }

    it('should create cache directory if not exists', async function () {
      // Ensure no cache exists
      const cacheDir = getCacheDirPath();
      delete mockFileSystem[cacheDir];

      await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');

      // Should create directory
      assert(mockFileSystem[cacheDir], 'should create cache directory');
    });

    it('should handle dismissed versions correctly', async function () {
      // Set up cache with dismissed version - use dev_version for dev channel
      const cacheData = {
        last_check: Date.now() - 1000,
        latest_version: null,
        dev_version: '5.5.0',
        dismissed_version: '5.5.0'
      };
      mockFileSystem[getCacheFilePath()] = JSON.stringify(cacheData);

      const result = await updateCheckerModule.checkForUpdates('5.4.1', false, 'npm', 'dev');

      // Should not show update for dismissed version
      assert.strictEqual(result.status, 'no_update');
      assert.strictEqual(result.reason, 'dismissed');
    });

    it('should handle corrupted cache gracefully', async function () {
      // Set up corrupted cache
      mockFileSystem[getCacheFilePath()] = 'invalid json';

      // Should not throw error
      const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');

      // Should work normally
      assert(result.status === 'update_available' || result.status === 'no_update');
    });
  });
});
